@modelprofile.com/browser-runtime 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.smartconfig.json +34 -0
- package/changelog.md +11 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/actions.d.ts +3 -0
- package/dist_ts/actions.js +215 -0
- package/dist_ts/classes.artifactstore.d.ts +38 -0
- package/dist_ts/classes.artifactstore.js +344 -0
- package/dist_ts/classes.egressproxy.d.ts +67 -0
- package/dist_ts/classes.egressproxy.js +830 -0
- package/dist_ts/classes.flexprovider.d.ts +9 -0
- package/dist_ts/classes.flexprovider.js +117 -0
- package/dist_ts/classes.framed.d.ts +52 -0
- package/dist_ts/classes.framed.js +557 -0
- package/dist_ts/classes.runtime.d.ts +202 -0
- package/dist_ts/classes.runtime.js +1667 -0
- package/dist_ts/confinement.d.ts +2 -0
- package/dist_ts/confinement.js +63 -0
- package/dist_ts/errors.d.ts +7 -0
- package/dist_ts/errors.js +40 -0
- package/dist_ts/index.d.ts +11 -0
- package/dist_ts/index.js +9 -0
- package/dist_ts/interfaces.d.ts +287 -0
- package/dist_ts/interfaces.js +2 -0
- package/dist_ts/internal.testing.d.ts +9 -0
- package/dist_ts/internal.testing.js +2 -0
- package/dist_ts/mcp.d.ts +4 -0
- package/dist_ts/mcp.js +196 -0
- package/dist_ts/plugins.d.ts +20 -0
- package/dist_ts/plugins.js +24 -0
- package/dist_ts/utils.d.ts +25 -0
- package/dist_ts/utils.js +143 -0
- package/license.md +21 -0
- package/package.json +59 -0
- package/readme.hints.md +35 -0
- package/readme.md +181 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/actions.ts +241 -0
- package/ts/classes.artifactstore.ts +432 -0
- package/ts/classes.egressproxy.ts +1005 -0
- package/ts/classes.flexprovider.ts +134 -0
- package/ts/classes.framed.ts +649 -0
- package/ts/classes.runtime.ts +2135 -0
- package/ts/confinement.ts +90 -0
- package/ts/errors.ts +63 -0
- package/ts/index.ts +52 -0
- package/ts/interfaces.ts +375 -0
- package/ts/internal.testing.ts +18 -0
- package/ts/mcp.ts +230 -0
- package/ts/plugins.ts +28 -0
- package/ts/utils.ts +188 -0
|
@@ -0,0 +1,2135 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { validateAgentAction } from './actions.js';
|
|
3
|
+
import { BrowserArtifactStore } from './classes.artifactstore.js';
|
|
4
|
+
import { BrowserEgressProxy } from './classes.egressproxy.js';
|
|
5
|
+
import { runProductionConfinementProbe } from './confinement.js';
|
|
6
|
+
import { BrowserRuntimeError } from './errors.js';
|
|
7
|
+
import {
|
|
8
|
+
browserRuntimeTesting,
|
|
9
|
+
type IBrowserRuntimeTestingOptions,
|
|
10
|
+
} from './internal.testing.js';
|
|
11
|
+
import type {
|
|
12
|
+
IAttachTrustedFramedPeerOptions,
|
|
13
|
+
IBrowserActionStateResult,
|
|
14
|
+
IBrowserArtifactMetadata,
|
|
15
|
+
IBrowserCapabilityDescriptor,
|
|
16
|
+
IBrowserConfinementProbeContext,
|
|
17
|
+
IBrowserObservationResult,
|
|
18
|
+
IBrowserRuntimeAuditEvent,
|
|
19
|
+
IBrowserRuntimeFrameSubscription,
|
|
20
|
+
IBrowserRuntimeOperationOptions,
|
|
21
|
+
IBrowserRuntimeOptions,
|
|
22
|
+
IBrowserRuntimeState,
|
|
23
|
+
IBrowserScreenshotResult,
|
|
24
|
+
IIssueBrowserCapabilityRequest,
|
|
25
|
+
IIssuedBrowserCapability,
|
|
26
|
+
ILiveBrowserSessionLike,
|
|
27
|
+
TBrowserActorRole,
|
|
28
|
+
TBrowserAgentAction,
|
|
29
|
+
TBrowserAgentActionResult,
|
|
30
|
+
TBrowserCapabilitySource,
|
|
31
|
+
TBrowserRuntimeEvent,
|
|
32
|
+
} from './interfaces.js';
|
|
33
|
+
import {
|
|
34
|
+
TransitionMutex,
|
|
35
|
+
digestToken,
|
|
36
|
+
digestsEqual,
|
|
37
|
+
randomId,
|
|
38
|
+
truncateString,
|
|
39
|
+
validateBoundedString,
|
|
40
|
+
validateInteger,
|
|
41
|
+
validateOptionalInteger,
|
|
42
|
+
waitBounded,
|
|
43
|
+
} from './utils.js';
|
|
44
|
+
import { BrowserRuntimeFramedServerPeer } from './classes.framed.js';
|
|
45
|
+
|
|
46
|
+
interface INormalizedRuntimeOptions {
|
|
47
|
+
runtimeDirectory: string;
|
|
48
|
+
authorizeCapability: IBrowserRuntimeOptions['authorizeCapability'];
|
|
49
|
+
audit?: IBrowserRuntimeOptions['audit'];
|
|
50
|
+
sessionFactory: (
|
|
51
|
+
options: plugins.smartpuppeteer.ILiveBrowserSessionOptions,
|
|
52
|
+
) => ILiveBrowserSessionLike;
|
|
53
|
+
confinementProbe: (context: IBrowserConfinementProbeContext) => Promise<void>;
|
|
54
|
+
environment: { platform: NodeJS.Platform; uid: number | undefined };
|
|
55
|
+
maxProjectSlots: number;
|
|
56
|
+
maxCapabilities: number;
|
|
57
|
+
maxCapabilitiesPerProject: number;
|
|
58
|
+
capabilityDefaultTtlMs: number;
|
|
59
|
+
capabilityMaximumTtlMs: number;
|
|
60
|
+
authorizationTimeoutMs: number;
|
|
61
|
+
maxTabsPerProject: number;
|
|
62
|
+
operationTimeoutMs: number;
|
|
63
|
+
quiescenceTimeoutMs: number;
|
|
64
|
+
terminationGraceMs: number;
|
|
65
|
+
terminationForceMs: number;
|
|
66
|
+
auditTimeoutMs: number;
|
|
67
|
+
confinementProbeTimeoutMs: number;
|
|
68
|
+
idleRetirementMs: number;
|
|
69
|
+
maxFrameBytes: number;
|
|
70
|
+
frameAcknowledgementTimeoutMs: number;
|
|
71
|
+
egress: NonNullable<IBrowserRuntimeOptions['egress']>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface ICapabilityRecord extends IBrowserCapabilityDescriptor {
|
|
75
|
+
digest: plugins.Buffer;
|
|
76
|
+
state: 'active' | 'revoked' | 'expired';
|
|
77
|
+
expiryTimer: ReturnType<typeof setTimeout>;
|
|
78
|
+
disconnect?: (signal: AbortSignal) => Promise<void> | void;
|
|
79
|
+
lease?: ILeaseRecord;
|
|
80
|
+
revocationPromise?: Promise<void>;
|
|
81
|
+
revocationInvokeDisconnect?: boolean;
|
|
82
|
+
revocationComplete?: boolean;
|
|
83
|
+
leaseCleanupComplete?: boolean;
|
|
84
|
+
disconnectComplete?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface ILeaseRecord {
|
|
88
|
+
leaseId: string;
|
|
89
|
+
capability: ICapabilityRecord;
|
|
90
|
+
digest: plugins.Buffer;
|
|
91
|
+
peerId: string;
|
|
92
|
+
role: TBrowserActorRole;
|
|
93
|
+
slot: IProjectSlot;
|
|
94
|
+
controller: AbortController;
|
|
95
|
+
released: boolean;
|
|
96
|
+
releasePromise?: Promise<void>;
|
|
97
|
+
releaseGeneration?: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface IOperationRecord {
|
|
101
|
+
operationId: string;
|
|
102
|
+
lease: ILeaseRecord;
|
|
103
|
+
controller: AbortController;
|
|
104
|
+
action: string;
|
|
105
|
+
startedAt: number;
|
|
106
|
+
promise: Promise<void>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface IFrameSubscriptionRecord {
|
|
110
|
+
lease: ILeaseRecord;
|
|
111
|
+
listener: (event: TBrowserRuntimeEvent) => void;
|
|
112
|
+
outstanding?: {
|
|
113
|
+
acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest;
|
|
114
|
+
timer: ReturnType<typeof setTimeout>;
|
|
115
|
+
};
|
|
116
|
+
closed: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface IProjectSlot {
|
|
120
|
+
projectId: string;
|
|
121
|
+
mutex: TransitionMutex;
|
|
122
|
+
generation: number;
|
|
123
|
+
fencingGeneration?: number;
|
|
124
|
+
permanentlyFenced: boolean;
|
|
125
|
+
session?: ILiveBrowserSessionLike;
|
|
126
|
+
proxy?: BrowserEgressProxy;
|
|
127
|
+
profileDirectory?: string;
|
|
128
|
+
unsubscribeSession?: () => void;
|
|
129
|
+
lease?: ILeaseRecord;
|
|
130
|
+
operation?: IOperationRecord;
|
|
131
|
+
frameSubscription?: IFrameSubscriptionRecord;
|
|
132
|
+
idleTimer?: ReturnType<typeof setTimeout>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface IAcquireBrowserRuntimeLeaseOptions {
|
|
136
|
+
capabilityToken: string;
|
|
137
|
+
peerId: string;
|
|
138
|
+
expectedRole?: TBrowserActorRole;
|
|
139
|
+
expectedSource?: TBrowserCapabilitySource;
|
|
140
|
+
scopeId?: string;
|
|
141
|
+
sessionId?: string;
|
|
142
|
+
signal?: AbortSignal;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const projectIdMaximum = 128;
|
|
146
|
+
const actorIdMaximum = 128;
|
|
147
|
+
const peerIdMaximum = 128;
|
|
148
|
+
|
|
149
|
+
export class BrowserRuntime {
|
|
150
|
+
private readonly options: INormalizedRuntimeOptions;
|
|
151
|
+
private readonly profileRoot: string;
|
|
152
|
+
private readonly artifactRoot: string;
|
|
153
|
+
private readonly lockPath: string;
|
|
154
|
+
private readonly artifactStore: BrowserArtifactStore;
|
|
155
|
+
private readonly slots = new Map<string, IProjectSlot>();
|
|
156
|
+
private readonly capabilitiesByDigest = new Map<string, ICapabilityRecord>();
|
|
157
|
+
private readonly capabilitiesById = new Map<string, ICapabilityRecord>();
|
|
158
|
+
private readonly failedRevocations = new Set<ICapabilityRecord>();
|
|
159
|
+
private readonly capabilityMutex = new TransitionMutex();
|
|
160
|
+
private readonly cleanupOperations = new Set<Promise<void>>();
|
|
161
|
+
private readonly framedPeers = new Set<BrowserRuntimeFramedServerPeer>();
|
|
162
|
+
private lockHandle?: plugins.fsPromises.FileHandle;
|
|
163
|
+
private lockHandleClosed = false;
|
|
164
|
+
private startPromise?: Promise<void>;
|
|
165
|
+
private stopPromise?: Promise<void>;
|
|
166
|
+
private lifecycleState: 'stopped' | 'starting' | 'running' | 'stopping' = 'stopped';
|
|
167
|
+
private lifecycleEpoch = 0;
|
|
168
|
+
private lifecycleController = new AbortController();
|
|
169
|
+
private pendingCapabilities = 0;
|
|
170
|
+
private readonly pendingCapabilitiesByProject = new Map<string, number>();
|
|
171
|
+
|
|
172
|
+
constructor(options: IBrowserRuntimeOptions | IBrowserRuntimeTestingOptions) {
|
|
173
|
+
if (!options || typeof options !== 'object') {
|
|
174
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
175
|
+
}
|
|
176
|
+
if (!plugins.path.isAbsolute(options.runtimeDirectory)) {
|
|
177
|
+
throw new BrowserRuntimeError('INVALID_INPUT', 'runtimeDirectory must be absolute');
|
|
178
|
+
}
|
|
179
|
+
if (typeof options.authorizeCapability !== 'function') {
|
|
180
|
+
throw new BrowserRuntimeError('INVALID_INPUT', 'authorizeCapability is required');
|
|
181
|
+
}
|
|
182
|
+
if (options.audit !== undefined && typeof options.audit !== 'function') {
|
|
183
|
+
throw new BrowserRuntimeError('INVALID_INPUT', 'audit must be a function');
|
|
184
|
+
}
|
|
185
|
+
const testingOptions = browserRuntimeTesting in options
|
|
186
|
+
&& options[browserRuntimeTesting] === true
|
|
187
|
+
? options as IBrowserRuntimeTestingOptions
|
|
188
|
+
: undefined;
|
|
189
|
+
const capabilityMaximumTtlMs = validateOptionalInteger(
|
|
190
|
+
options.capabilityMaximumTtlMs,
|
|
191
|
+
'capabilityMaximumTtlMs',
|
|
192
|
+
60_000,
|
|
193
|
+
24 * 60 * 60 * 1000,
|
|
194
|
+
24 * 60 * 60 * 1000,
|
|
195
|
+
);
|
|
196
|
+
const capabilityDefaultTtlMs = validateOptionalInteger(
|
|
197
|
+
options.capabilityDefaultTtlMs,
|
|
198
|
+
'capabilityDefaultTtlMs',
|
|
199
|
+
1000,
|
|
200
|
+
capabilityMaximumTtlMs,
|
|
201
|
+
5 * 60_000,
|
|
202
|
+
);
|
|
203
|
+
const environment = testingOptions?.environment ?? {
|
|
204
|
+
platform: process.platform,
|
|
205
|
+
uid: process.getuid?.(),
|
|
206
|
+
};
|
|
207
|
+
if (
|
|
208
|
+
!environment
|
|
209
|
+
|| typeof environment.platform !== 'string'
|
|
210
|
+
|| (environment.uid !== undefined && !Number.isInteger(environment.uid))
|
|
211
|
+
) {
|
|
212
|
+
throw new BrowserRuntimeError('INVALID_INPUT', 'environment is invalid');
|
|
213
|
+
}
|
|
214
|
+
this.options = {
|
|
215
|
+
runtimeDirectory: options.runtimeDirectory,
|
|
216
|
+
authorizeCapability: options.authorizeCapability,
|
|
217
|
+
audit: options.audit,
|
|
218
|
+
sessionFactory: testingOptions?.sessionFactory ?? ((sessionOptions) => (
|
|
219
|
+
new plugins.smartpuppeteer.LiveBrowserSession(sessionOptions)
|
|
220
|
+
)),
|
|
221
|
+
confinementProbe: testingOptions?.confinementProbe ?? runProductionConfinementProbe,
|
|
222
|
+
environment,
|
|
223
|
+
maxProjectSlots: validateOptionalInteger(
|
|
224
|
+
options.maxProjectSlots,
|
|
225
|
+
'maxProjectSlots',
|
|
226
|
+
1,
|
|
227
|
+
256,
|
|
228
|
+
32,
|
|
229
|
+
),
|
|
230
|
+
maxCapabilities: validateOptionalInteger(
|
|
231
|
+
options.maxCapabilities,
|
|
232
|
+
'maxCapabilities',
|
|
233
|
+
1,
|
|
234
|
+
4096,
|
|
235
|
+
512,
|
|
236
|
+
),
|
|
237
|
+
maxCapabilitiesPerProject: validateOptionalInteger(
|
|
238
|
+
options.maxCapabilitiesPerProject,
|
|
239
|
+
'maxCapabilitiesPerProject',
|
|
240
|
+
1,
|
|
241
|
+
512,
|
|
242
|
+
64,
|
|
243
|
+
),
|
|
244
|
+
capabilityDefaultTtlMs,
|
|
245
|
+
capabilityMaximumTtlMs,
|
|
246
|
+
authorizationTimeoutMs: validateOptionalInteger(
|
|
247
|
+
options.authorizationTimeoutMs,
|
|
248
|
+
'authorizationTimeoutMs',
|
|
249
|
+
100,
|
|
250
|
+
120_000,
|
|
251
|
+
10_000,
|
|
252
|
+
),
|
|
253
|
+
maxTabsPerProject: validateOptionalInteger(
|
|
254
|
+
options.maxTabsPerProject,
|
|
255
|
+
'maxTabsPerProject',
|
|
256
|
+
1,
|
|
257
|
+
256,
|
|
258
|
+
32,
|
|
259
|
+
),
|
|
260
|
+
operationTimeoutMs: validateOptionalInteger(
|
|
261
|
+
options.operationTimeoutMs,
|
|
262
|
+
'operationTimeoutMs',
|
|
263
|
+
100,
|
|
264
|
+
120_000,
|
|
265
|
+
30_000,
|
|
266
|
+
),
|
|
267
|
+
quiescenceTimeoutMs: validateOptionalInteger(
|
|
268
|
+
options.quiescenceTimeoutMs,
|
|
269
|
+
'quiescenceTimeoutMs',
|
|
270
|
+
100,
|
|
271
|
+
60_000,
|
|
272
|
+
5000,
|
|
273
|
+
),
|
|
274
|
+
terminationGraceMs: validateOptionalInteger(
|
|
275
|
+
options.terminationGraceMs,
|
|
276
|
+
'terminationGraceMs',
|
|
277
|
+
100,
|
|
278
|
+
60_000,
|
|
279
|
+
5000,
|
|
280
|
+
),
|
|
281
|
+
terminationForceMs: validateOptionalInteger(
|
|
282
|
+
options.terminationForceMs,
|
|
283
|
+
'terminationForceMs',
|
|
284
|
+
100,
|
|
285
|
+
60_000,
|
|
286
|
+
5000,
|
|
287
|
+
),
|
|
288
|
+
auditTimeoutMs: validateOptionalInteger(
|
|
289
|
+
options.auditTimeoutMs,
|
|
290
|
+
'auditTimeoutMs',
|
|
291
|
+
100,
|
|
292
|
+
30_000,
|
|
293
|
+
2000,
|
|
294
|
+
),
|
|
295
|
+
confinementProbeTimeoutMs: validateOptionalInteger(
|
|
296
|
+
options.confinementProbeTimeoutMs,
|
|
297
|
+
'confinementProbeTimeoutMs',
|
|
298
|
+
1000,
|
|
299
|
+
60_000,
|
|
300
|
+
10_000,
|
|
301
|
+
),
|
|
302
|
+
idleRetirementMs: validateOptionalInteger(
|
|
303
|
+
options.idleRetirementMs,
|
|
304
|
+
'idleRetirementMs',
|
|
305
|
+
1000,
|
|
306
|
+
24 * 60 * 60 * 1000,
|
|
307
|
+
15 * 60_000,
|
|
308
|
+
),
|
|
309
|
+
maxFrameBytes: validateOptionalInteger(
|
|
310
|
+
options.maxFrameBytes,
|
|
311
|
+
'maxFrameBytes',
|
|
312
|
+
64 * 1024,
|
|
313
|
+
16 * 1024 * 1024,
|
|
314
|
+
4 * 1024 * 1024,
|
|
315
|
+
),
|
|
316
|
+
frameAcknowledgementTimeoutMs: validateOptionalInteger(
|
|
317
|
+
options.frameAcknowledgementTimeoutMs,
|
|
318
|
+
'frameAcknowledgementTimeoutMs',
|
|
319
|
+
100,
|
|
320
|
+
60_000,
|
|
321
|
+
10_000,
|
|
322
|
+
),
|
|
323
|
+
egress: options.egress ?? {},
|
|
324
|
+
};
|
|
325
|
+
this.profileRoot = plugins.path.join(options.runtimeDirectory, 'profiles');
|
|
326
|
+
this.artifactRoot = plugins.path.join(options.runtimeDirectory, 'artifacts');
|
|
327
|
+
this.lockPath = plugins.path.join(options.runtimeDirectory, 'runtime.lock');
|
|
328
|
+
this.artifactStore = new BrowserArtifactStore({
|
|
329
|
+
rootDirectory: this.artifactRoot,
|
|
330
|
+
...(options.artifacts ?? {}),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
public start(): Promise<void> {
|
|
335
|
+
if (this.lifecycleState === 'running') return Promise.resolve();
|
|
336
|
+
if (this.startPromise) return this.startPromise;
|
|
337
|
+
if (this.stopPromise) return this.stopPromise.then(() => this.start());
|
|
338
|
+
this.lifecycleState = 'starting';
|
|
339
|
+
const epoch = ++this.lifecycleEpoch;
|
|
340
|
+
this.lifecycleController = new AbortController();
|
|
341
|
+
this.startPromise = this.startInternal().then(() => {
|
|
342
|
+
if (
|
|
343
|
+
this.lifecycleState !== 'starting'
|
|
344
|
+
|| this.lifecycleEpoch !== epoch
|
|
345
|
+
|| this.lifecycleController.signal.aborted
|
|
346
|
+
) throw new BrowserRuntimeError('ABORTED');
|
|
347
|
+
this.lifecycleState = 'running';
|
|
348
|
+
}).catch((error) => {
|
|
349
|
+
if (this.lifecycleState === 'starting') this.lifecycleState = 'stopped';
|
|
350
|
+
throw error;
|
|
351
|
+
}).finally(() => {
|
|
352
|
+
this.startPromise = undefined;
|
|
353
|
+
});
|
|
354
|
+
return this.startPromise;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
public stop(): Promise<void> {
|
|
358
|
+
if (this.stopPromise) return this.stopPromise;
|
|
359
|
+
if (this.lifecycleState === 'stopped' && !this.lockHandle && !this.startPromise) {
|
|
360
|
+
return Promise.resolve();
|
|
361
|
+
}
|
|
362
|
+
this.lifecycleState = 'stopping';
|
|
363
|
+
this.lifecycleEpoch += 1;
|
|
364
|
+
this.lifecycleController.abort(new BrowserRuntimeError('ABORTED'));
|
|
365
|
+
const startup = this.startPromise;
|
|
366
|
+
this.stopPromise = (async () => {
|
|
367
|
+
await startup?.catch(() => undefined);
|
|
368
|
+
await this.stopInternal();
|
|
369
|
+
this.lifecycleState = 'stopped';
|
|
370
|
+
})().finally(() => {
|
|
371
|
+
this.stopPromise = undefined;
|
|
372
|
+
});
|
|
373
|
+
return this.stopPromise;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
public async issueCapability(
|
|
377
|
+
requestArg: IIssueBrowserCapabilityRequest,
|
|
378
|
+
): Promise<IIssuedBrowserCapability> {
|
|
379
|
+
this.requireRunning();
|
|
380
|
+
if (!requestArg || typeof requestArg !== 'object') {
|
|
381
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
382
|
+
}
|
|
383
|
+
const projectId = validateBoundedString(
|
|
384
|
+
requestArg.projectId,
|
|
385
|
+
'projectId',
|
|
386
|
+
1,
|
|
387
|
+
projectIdMaximum,
|
|
388
|
+
);
|
|
389
|
+
const actorId = validateBoundedString(requestArg.actorId, 'actorId', 1, actorIdMaximum);
|
|
390
|
+
const peerId = validateBoundedString(requestArg.peerId, 'peerId', 1, peerIdMaximum);
|
|
391
|
+
const role = this.validateRole(requestArg.role);
|
|
392
|
+
const source = this.validateSource(requestArg.source);
|
|
393
|
+
if (
|
|
394
|
+
(role === 'human' && source !== 'human')
|
|
395
|
+
|| (role === 'agent' && source === 'human')
|
|
396
|
+
) {
|
|
397
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
398
|
+
}
|
|
399
|
+
if (requestArg.disconnect !== undefined && typeof requestArg.disconnect !== 'function') {
|
|
400
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
401
|
+
}
|
|
402
|
+
const disconnect = requestArg.disconnect;
|
|
403
|
+
const scopeId = requestArg.scopeId === undefined
|
|
404
|
+
? undefined
|
|
405
|
+
: validateBoundedString(requestArg.scopeId, 'scopeId', 1, 128);
|
|
406
|
+
const sessionId = requestArg.sessionId === undefined
|
|
407
|
+
? undefined
|
|
408
|
+
: validateBoundedString(requestArg.sessionId, 'sessionId', 1, 128);
|
|
409
|
+
if (
|
|
410
|
+
(source === 'flex' && (!scopeId || !sessionId))
|
|
411
|
+
|| (source !== 'flex' && (scopeId !== undefined || sessionId !== undefined))
|
|
412
|
+
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
413
|
+
const expiresInMs = requestArg.expiresInMs === undefined
|
|
414
|
+
? this.options.capabilityDefaultTtlMs
|
|
415
|
+
: validateInteger(
|
|
416
|
+
requestArg.expiresInMs,
|
|
417
|
+
'expiresInMs',
|
|
418
|
+
1000,
|
|
419
|
+
this.options.capabilityMaximumTtlMs,
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const lifecycleEpoch = this.lifecycleEpoch;
|
|
423
|
+
const pendingForProject = this.pendingCapabilitiesByProject.get(projectId) ?? 0;
|
|
424
|
+
const activeForProject = [...this.capabilitiesById.values()].filter((record) => (
|
|
425
|
+
record.projectId === projectId && record.state === 'active'
|
|
426
|
+
)).length;
|
|
427
|
+
if (
|
|
428
|
+
this.pendingCapabilities + this.capabilitiesById.size >= this.options.maxCapabilities
|
|
429
|
+
|| pendingForProject + activeForProject >= this.options.maxCapabilitiesPerProject
|
|
430
|
+
) throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
431
|
+
this.pendingCapabilities += 1;
|
|
432
|
+
this.pendingCapabilitiesByProject.set(projectId, pendingForProject + 1);
|
|
433
|
+
let authorized = false;
|
|
434
|
+
const authorizationController = new AbortController();
|
|
435
|
+
const authorizationSignal = AbortSignal.any([
|
|
436
|
+
authorizationController.signal,
|
|
437
|
+
this.lifecycleController.signal,
|
|
438
|
+
]);
|
|
439
|
+
try {
|
|
440
|
+
const authorization = Promise.resolve(this.options.authorizeCapability({
|
|
441
|
+
projectId,
|
|
442
|
+
actorId,
|
|
443
|
+
role,
|
|
444
|
+
peerId,
|
|
445
|
+
source,
|
|
446
|
+
...(scopeId ? { scopeId } : {}),
|
|
447
|
+
...(sessionId ? { sessionId } : {}),
|
|
448
|
+
}, authorizationSignal));
|
|
449
|
+
const result = await waitBounded(authorization, this.options.authorizationTimeoutMs);
|
|
450
|
+
if (!result.settled) {
|
|
451
|
+
authorizationController.abort(new BrowserRuntimeError('TIMEOUT'));
|
|
452
|
+
throw new BrowserRuntimeError('TIMEOUT');
|
|
453
|
+
}
|
|
454
|
+
authorized = result.value;
|
|
455
|
+
} catch {
|
|
456
|
+
throw new BrowserRuntimeError('AUTHORIZATION_DENIED');
|
|
457
|
+
} finally {
|
|
458
|
+
authorizationController.abort(new BrowserRuntimeError('ABORTED'));
|
|
459
|
+
this.pendingCapabilities -= 1;
|
|
460
|
+
const remaining = (this.pendingCapabilitiesByProject.get(projectId) ?? 1) - 1;
|
|
461
|
+
if (remaining > 0) this.pendingCapabilitiesByProject.set(projectId, remaining);
|
|
462
|
+
else this.pendingCapabilitiesByProject.delete(projectId);
|
|
463
|
+
}
|
|
464
|
+
if (authorized !== true) throw new BrowserRuntimeError('AUTHORIZATION_DENIED');
|
|
465
|
+
this.requireRunning();
|
|
466
|
+
if (this.lifecycleEpoch !== lifecycleEpoch) throw new BrowserRuntimeError('ABORTED');
|
|
467
|
+
|
|
468
|
+
const release = await this.capabilityMutex.acquire();
|
|
469
|
+
try {
|
|
470
|
+
this.requireRunning();
|
|
471
|
+
if (this.lifecycleEpoch !== lifecycleEpoch) throw new BrowserRuntimeError('ABORTED');
|
|
472
|
+
const activeProjectCapabilities = [...this.capabilitiesById.values()].filter((record) => (
|
|
473
|
+
record.projectId === projectId && record.state === 'active'
|
|
474
|
+
)).length;
|
|
475
|
+
if (
|
|
476
|
+
this.capabilitiesById.size >= this.options.maxCapabilities
|
|
477
|
+
|| activeProjectCapabilities >= this.options.maxCapabilitiesPerProject
|
|
478
|
+
) {
|
|
479
|
+
throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
480
|
+
}
|
|
481
|
+
this.getOrCreateSlot(projectId);
|
|
482
|
+
let capabilityToken: string;
|
|
483
|
+
let digest: plugins.Buffer;
|
|
484
|
+
let digestKey: string;
|
|
485
|
+
do {
|
|
486
|
+
capabilityToken = randomId(32);
|
|
487
|
+
digest = digestToken(capabilityToken);
|
|
488
|
+
digestKey = digest.toString('hex');
|
|
489
|
+
} while (this.capabilitiesByDigest.has(digestKey));
|
|
490
|
+
const capabilityId = randomId(18);
|
|
491
|
+
const expiresAt = Date.now() + expiresInMs;
|
|
492
|
+
let record: ICapabilityRecord;
|
|
493
|
+
const expiryTimer = setTimeout(() => {
|
|
494
|
+
if (record.state === 'active') record.state = 'expired';
|
|
495
|
+
this.trackCleanup(this.revokeCapabilityRecord(record));
|
|
496
|
+
}, expiresInMs);
|
|
497
|
+
record = {
|
|
498
|
+
capabilityId,
|
|
499
|
+
projectId,
|
|
500
|
+
actorId,
|
|
501
|
+
role,
|
|
502
|
+
peerId,
|
|
503
|
+
source,
|
|
504
|
+
...(scopeId ? { scopeId } : {}),
|
|
505
|
+
...(sessionId ? { sessionId } : {}),
|
|
506
|
+
expiresAt,
|
|
507
|
+
digest,
|
|
508
|
+
state: 'active',
|
|
509
|
+
disconnect,
|
|
510
|
+
expiryTimer,
|
|
511
|
+
};
|
|
512
|
+
record.expiryTimer.unref();
|
|
513
|
+
this.capabilitiesByDigest.set(digestKey, record);
|
|
514
|
+
this.capabilitiesById.set(capabilityId, record);
|
|
515
|
+
return {
|
|
516
|
+
capabilityId,
|
|
517
|
+
projectId,
|
|
518
|
+
actorId,
|
|
519
|
+
role,
|
|
520
|
+
peerId,
|
|
521
|
+
source,
|
|
522
|
+
...(scopeId ? { scopeId } : {}),
|
|
523
|
+
...(sessionId ? { sessionId } : {}),
|
|
524
|
+
expiresAt,
|
|
525
|
+
capabilityToken,
|
|
526
|
+
};
|
|
527
|
+
} finally {
|
|
528
|
+
release();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
public async acquireLease(
|
|
533
|
+
optionsArg: IAcquireBrowserRuntimeLeaseOptions,
|
|
534
|
+
): Promise<BrowserRuntimeLease> {
|
|
535
|
+
this.requireRunning();
|
|
536
|
+
if (!optionsArg || typeof optionsArg !== 'object') {
|
|
537
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
538
|
+
}
|
|
539
|
+
const peerId = validateBoundedString(optionsArg.peerId, 'peerId', 1, peerIdMaximum);
|
|
540
|
+
const expectedRole = optionsArg.expectedRole === undefined
|
|
541
|
+
? undefined
|
|
542
|
+
: this.validateRole(optionsArg.expectedRole);
|
|
543
|
+
const expectedSource = optionsArg.expectedSource === undefined
|
|
544
|
+
? undefined
|
|
545
|
+
: this.validateSource(optionsArg.expectedSource);
|
|
546
|
+
const scopeId = optionsArg.scopeId === undefined
|
|
547
|
+
? undefined
|
|
548
|
+
: validateBoundedString(optionsArg.scopeId, 'scopeId', 1, 128);
|
|
549
|
+
const sessionId = optionsArg.sessionId === undefined
|
|
550
|
+
? undefined
|
|
551
|
+
: validateBoundedString(optionsArg.sessionId, 'sessionId', 1, 128);
|
|
552
|
+
this.throwIfAcquisitionAborted(optionsArg.signal);
|
|
553
|
+
const capabilityToken = validateBoundedString(
|
|
554
|
+
optionsArg.capabilityToken,
|
|
555
|
+
'capabilityToken',
|
|
556
|
+
16,
|
|
557
|
+
512,
|
|
558
|
+
);
|
|
559
|
+
const digest = digestToken(capabilityToken);
|
|
560
|
+
const capability = this.resolveCapability(
|
|
561
|
+
digest,
|
|
562
|
+
peerId,
|
|
563
|
+
expectedRole,
|
|
564
|
+
expectedSource,
|
|
565
|
+
scopeId,
|
|
566
|
+
sessionId,
|
|
567
|
+
);
|
|
568
|
+
const slot = this.getOrCreateSlot(capability.projectId);
|
|
569
|
+
return capability.role === 'agent'
|
|
570
|
+
? this.acquireAgentLease(slot, capability, digest, optionsArg)
|
|
571
|
+
: this.acquireHumanLease(slot, capability, digest, optionsArg);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
public authorizeCapability(
|
|
575
|
+
capabilityTokenArg: string,
|
|
576
|
+
peerIdArg: string,
|
|
577
|
+
role: TBrowserActorRole,
|
|
578
|
+
source: TBrowserCapabilitySource,
|
|
579
|
+
): IBrowserCapabilityDescriptor {
|
|
580
|
+
this.requireRunning();
|
|
581
|
+
const token = validateBoundedString(capabilityTokenArg, 'capabilityToken', 16, 512);
|
|
582
|
+
const peerId = validateBoundedString(peerIdArg, 'peerId', 1, peerIdMaximum);
|
|
583
|
+
const record = this.resolveCapability(digestToken(token), peerId, role, source);
|
|
584
|
+
return this.describeCapability(record);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
public async revokeCapability(capabilityIdArg: string): Promise<void> {
|
|
588
|
+
const capabilityId = validateBoundedString(
|
|
589
|
+
capabilityIdArg,
|
|
590
|
+
'capabilityId',
|
|
591
|
+
1,
|
|
592
|
+
128,
|
|
593
|
+
);
|
|
594
|
+
const capability = this.capabilitiesById.get(capabilityId);
|
|
595
|
+
if (!capability) return;
|
|
596
|
+
await this.revokeCapabilityRecord(capability);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
public async disconnectPeer(peerIdArg: string): Promise<void> {
|
|
600
|
+
const peerId = validateBoundedString(peerIdArg, 'peerId', 1, peerIdMaximum);
|
|
601
|
+
const matching = [...this.capabilitiesById.values()].filter((record) => (
|
|
602
|
+
record.peerId === peerId && record.state === 'active'
|
|
603
|
+
));
|
|
604
|
+
const results = await Promise.allSettled(
|
|
605
|
+
matching.map((record) => this.revokeCapabilityRecord(record, false)),
|
|
606
|
+
);
|
|
607
|
+
const errors = results
|
|
608
|
+
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
609
|
+
.map((result) => result.reason);
|
|
610
|
+
if (errors.length > 0) throw new AggregateError(errors, 'Peer cleanup is incomplete');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
public async retireProject(projectIdArg: string): Promise<void> {
|
|
614
|
+
const projectId = validateBoundedString(projectIdArg, 'projectId', 1, projectIdMaximum);
|
|
615
|
+
const capabilities = [...this.capabilitiesById.values()].filter((record) => (
|
|
616
|
+
record.projectId === projectId
|
|
617
|
+
));
|
|
618
|
+
const results = await Promise.allSettled(
|
|
619
|
+
capabilities.map((record) => this.revokeCapabilityRecord(record)),
|
|
620
|
+
);
|
|
621
|
+
const errors = results
|
|
622
|
+
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
623
|
+
.map((result) => result.reason);
|
|
624
|
+
if (errors.length > 0) throw new AggregateError(errors, 'Project capability cleanup is incomplete');
|
|
625
|
+
await this.retireSlot(projectId, true);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
public attachTrustedFramedPeer(
|
|
629
|
+
options: IAttachTrustedFramedPeerOptions,
|
|
630
|
+
): BrowserRuntimeFramedServerPeer {
|
|
631
|
+
this.requireRunning();
|
|
632
|
+
const peerId = validateBoundedString(options.peerId, 'peerId', 1, peerIdMaximum);
|
|
633
|
+
if ([...this.framedPeers].some((peer) => peer.peerId === peerId)) {
|
|
634
|
+
throw new BrowserRuntimeError('BUSY');
|
|
635
|
+
}
|
|
636
|
+
const peer = new BrowserRuntimeFramedServerPeer(this, options, () => {
|
|
637
|
+
this.framedPeers.delete(peer);
|
|
638
|
+
});
|
|
639
|
+
this.framedPeers.add(peer);
|
|
640
|
+
peer.start();
|
|
641
|
+
return peer;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
public getArtifactStore(): BrowserArtifactStore {
|
|
645
|
+
return this.artifactStore;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** @internal */
|
|
649
|
+
public getLeaseState(lease: ILeaseRecord): IBrowserRuntimeState {
|
|
650
|
+
this.requireValidLease(lease);
|
|
651
|
+
const session = lease.slot.session!;
|
|
652
|
+
return this.projectState(session.getState());
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** @internal */
|
|
656
|
+
public async executeLeaseAgentAction(
|
|
657
|
+
lease: ILeaseRecord,
|
|
658
|
+
actionArg: unknown,
|
|
659
|
+
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
660
|
+
): Promise<TBrowserAgentActionResult> {
|
|
661
|
+
if (lease.role !== 'agent') throw new BrowserRuntimeError('CAPABILITY_INVALID');
|
|
662
|
+
const action = validateAgentAction(actionArg);
|
|
663
|
+
return this.runOperation(lease, action.action, operationOptions, async (signal, session) => (
|
|
664
|
+
this.executeAgentActionInternal(lease, session, action, signal)
|
|
665
|
+
));
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** @internal */
|
|
669
|
+
public async leaseCreateTab(
|
|
670
|
+
lease: ILeaseRecord,
|
|
671
|
+
options: plugins.smartpuppeteer.ILiveBrowserCreateTabOptions = {},
|
|
672
|
+
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
673
|
+
): Promise<IBrowserRuntimeState> {
|
|
674
|
+
this.requireHuman(lease);
|
|
675
|
+
return this.runOperation(lease, 'createTab', operationOptions, async (signal, session) => {
|
|
676
|
+
if (session.getState().tabs.length >= this.options.maxTabsPerProject) {
|
|
677
|
+
throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
678
|
+
}
|
|
679
|
+
await session.createTab(options, { signal });
|
|
680
|
+
return this.projectState(session.getState());
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** @internal */
|
|
685
|
+
public async leaseActivateTab(
|
|
686
|
+
lease: ILeaseRecord,
|
|
687
|
+
tabIdArg: string,
|
|
688
|
+
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
689
|
+
): Promise<IBrowserRuntimeState> {
|
|
690
|
+
this.requireHuman(lease);
|
|
691
|
+
const tabId = validateBoundedString(tabIdArg, 'tabId', 1, 128);
|
|
692
|
+
return this.runOperation(lease, 'activateTab', operationOptions, async (signal, session) => {
|
|
693
|
+
await session.activateTab(tabId, { signal });
|
|
694
|
+
return this.projectState(session.getState());
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** @internal */
|
|
699
|
+
public async leaseCloseTab(
|
|
700
|
+
lease: ILeaseRecord,
|
|
701
|
+
tabIdArg: string,
|
|
702
|
+
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
703
|
+
): Promise<IBrowserRuntimeState> {
|
|
704
|
+
this.requireHuman(lease);
|
|
705
|
+
const tabId = validateBoundedString(tabIdArg, 'tabId', 1, 128);
|
|
706
|
+
return this.runOperation(lease, 'closeTab', operationOptions, async (signal, session) => {
|
|
707
|
+
await session.closeTab(tabId, { signal });
|
|
708
|
+
return this.projectState(session.getState());
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** @internal */
|
|
713
|
+
public async leaseHistory(
|
|
714
|
+
lease: ILeaseRecord,
|
|
715
|
+
action: 'back' | 'forward' | 'reload',
|
|
716
|
+
options: plugins.smartpuppeteer.ILiveBrowserNavigationOptions = {},
|
|
717
|
+
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
718
|
+
): Promise<IBrowserRuntimeState> {
|
|
719
|
+
this.requireHuman(lease);
|
|
720
|
+
return this.runOperation(lease, action, operationOptions, async (signal, session) => {
|
|
721
|
+
await session[action](options, { signal });
|
|
722
|
+
return this.projectState(session.getState());
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/** @internal */
|
|
727
|
+
public async leaseHumanAgentAction(
|
|
728
|
+
lease: ILeaseRecord,
|
|
729
|
+
actionArg: unknown,
|
|
730
|
+
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
731
|
+
): Promise<TBrowserAgentActionResult> {
|
|
732
|
+
this.requireHuman(lease);
|
|
733
|
+
const action = validateAgentAction(actionArg);
|
|
734
|
+
return this.runOperation(lease, action.action, operationOptions, async (signal, session) => (
|
|
735
|
+
this.executeAgentActionInternal(lease, session, action, signal)
|
|
736
|
+
));
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** @internal */
|
|
740
|
+
public async leaseSetViewport(
|
|
741
|
+
lease: ILeaseRecord,
|
|
742
|
+
viewport: plugins.smartpuppeteer.ILiveBrowserViewport,
|
|
743
|
+
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
744
|
+
): Promise<void> {
|
|
745
|
+
this.requireHuman(lease);
|
|
746
|
+
await this.runOperation(lease, 'setViewport', operationOptions, async (signal, session) => {
|
|
747
|
+
await session.setViewport(viewport, { signal });
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** @internal */
|
|
752
|
+
public async leaseDispatchRawInput(
|
|
753
|
+
lease: ILeaseRecord,
|
|
754
|
+
action: 'dispatchMouse' | 'dispatchWheel' | 'dispatchKey' | 'insertText',
|
|
755
|
+
input:
|
|
756
|
+
| plugins.smartpuppeteer.ILiveBrowserMouseInput
|
|
757
|
+
| plugins.smartpuppeteer.ILiveBrowserWheelInput
|
|
758
|
+
| plugins.smartpuppeteer.ILiveBrowserKeyInput
|
|
759
|
+
| plugins.smartpuppeteer.ILiveBrowserInsertTextInput,
|
|
760
|
+
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
761
|
+
): Promise<void> {
|
|
762
|
+
this.requireHuman(lease);
|
|
763
|
+
await this.runOperation(lease, action, operationOptions, async (_signal, session) => {
|
|
764
|
+
if (action === 'dispatchMouse') {
|
|
765
|
+
await session.dispatchMouse(input as plugins.smartpuppeteer.ILiveBrowserMouseInput);
|
|
766
|
+
} else if (action === 'dispatchWheel') {
|
|
767
|
+
await session.dispatchWheel(input as plugins.smartpuppeteer.ILiveBrowserWheelInput);
|
|
768
|
+
} else if (action === 'dispatchKey') {
|
|
769
|
+
await session.dispatchKey(input as plugins.smartpuppeteer.ILiveBrowserKeyInput);
|
|
770
|
+
} else {
|
|
771
|
+
await session.insertText(input as plugins.smartpuppeteer.ILiveBrowserInsertTextInput);
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/** @internal */
|
|
777
|
+
public async subscribeLeaseEvents(
|
|
778
|
+
lease: ILeaseRecord,
|
|
779
|
+
listener: (event: TBrowserRuntimeEvent) => void,
|
|
780
|
+
): Promise<IBrowserRuntimeFrameSubscription> {
|
|
781
|
+
this.requireHuman(lease);
|
|
782
|
+
if (typeof listener !== 'function') throw new BrowserRuntimeError('INVALID_INPUT');
|
|
783
|
+
const slot = lease.slot;
|
|
784
|
+
const release = await slot.mutex.acquire();
|
|
785
|
+
let subscription: IFrameSubscriptionRecord;
|
|
786
|
+
try {
|
|
787
|
+
this.requireValidLease(lease);
|
|
788
|
+
if (slot.frameSubscription) throw new BrowserRuntimeError('BUSY');
|
|
789
|
+
subscription = { lease, listener, closed: false };
|
|
790
|
+
slot.frameSubscription = subscription;
|
|
791
|
+
this.pushToSubscription(subscription, {
|
|
792
|
+
type: 'state',
|
|
793
|
+
state: this.projectState(slot.session!.getState()),
|
|
794
|
+
});
|
|
795
|
+
} finally {
|
|
796
|
+
release();
|
|
797
|
+
}
|
|
798
|
+
return {
|
|
799
|
+
close: async () => this.closeFrameSubscription(subscription),
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** @internal */
|
|
804
|
+
public async acknowledgeLeaseFrame(
|
|
805
|
+
lease: ILeaseRecord,
|
|
806
|
+
acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest,
|
|
807
|
+
): Promise<boolean> {
|
|
808
|
+
this.requireHuman(lease);
|
|
809
|
+
if (!acknowledgement || typeof acknowledgement !== 'object') {
|
|
810
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
811
|
+
}
|
|
812
|
+
validateBoundedString(acknowledgement.tabId, 'tabId', 1, 128);
|
|
813
|
+
validateInteger(acknowledgement.sequence, 'sequence', 0, Number.MAX_SAFE_INTEGER);
|
|
814
|
+
validateInteger(acknowledgement.generation, 'generation', 0, Number.MAX_SAFE_INTEGER);
|
|
815
|
+
validateInteger(
|
|
816
|
+
acknowledgement.viewportRevision,
|
|
817
|
+
'viewportRevision',
|
|
818
|
+
0,
|
|
819
|
+
Number.MAX_SAFE_INTEGER,
|
|
820
|
+
);
|
|
821
|
+
const slot = lease.slot;
|
|
822
|
+
const subscription = slot.frameSubscription;
|
|
823
|
+
this.requireValidLease(lease);
|
|
824
|
+
if (!subscription || subscription.lease !== lease || !subscription.outstanding) return false;
|
|
825
|
+
const expected = subscription.outstanding.acknowledgement;
|
|
826
|
+
if (
|
|
827
|
+
expected.tabId !== acknowledgement.tabId
|
|
828
|
+
|| expected.sequence !== acknowledgement.sequence
|
|
829
|
+
|| expected.generation !== acknowledgement.generation
|
|
830
|
+
|| expected.viewportRevision !== acknowledgement.viewportRevision
|
|
831
|
+
) return false;
|
|
832
|
+
clearTimeout(subscription.outstanding.timer);
|
|
833
|
+
subscription.outstanding = undefined;
|
|
834
|
+
const session = slot.session!;
|
|
835
|
+
const operation = session.acknowledgeFrame(acknowledgement);
|
|
836
|
+
const result = await waitBounded(operation, this.options.frameAcknowledgementTimeoutMs)
|
|
837
|
+
.catch(() => ({ settled: true as const, value: { accepted: false } }));
|
|
838
|
+
if (!result.settled || !result.value.accepted) {
|
|
839
|
+
operation.catch(() => undefined);
|
|
840
|
+
const errors: unknown[] = [];
|
|
841
|
+
await this.revokeCapabilityRecord(lease.capability).catch((error) => errors.push(error));
|
|
842
|
+
if (slot.session === session) {
|
|
843
|
+
await this.terminateSlotSession(slot).catch((error) => errors.push(error));
|
|
844
|
+
}
|
|
845
|
+
if (errors.length === 1) throw errors[0];
|
|
846
|
+
if (errors.length > 1) {
|
|
847
|
+
throw new AggregateError(errors, 'Frame acknowledgement cleanup is incomplete');
|
|
848
|
+
}
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/** @internal */
|
|
855
|
+
public async readLeaseArtifact(
|
|
856
|
+
lease: ILeaseRecord,
|
|
857
|
+
artifactId: string,
|
|
858
|
+
): Promise<Uint8Array> {
|
|
859
|
+
this.requireHuman(lease);
|
|
860
|
+
this.requireValidLease(lease);
|
|
861
|
+
return this.artifactStore.read(lease.capability.projectId, artifactId);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/** @internal */
|
|
865
|
+
public async deleteLeaseArtifact(lease: ILeaseRecord, artifactId: string): Promise<void> {
|
|
866
|
+
this.requireHuman(lease);
|
|
867
|
+
this.requireValidLease(lease);
|
|
868
|
+
await this.artifactStore.delete(lease.capability.projectId, artifactId);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/** @internal */
|
|
872
|
+
public cancelLeaseOperation(lease: ILeaseRecord): boolean {
|
|
873
|
+
this.requireValidLease(lease);
|
|
874
|
+
const operation = lease.slot.operation;
|
|
875
|
+
if (!operation || operation.lease !== lease) return false;
|
|
876
|
+
operation.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/** @internal */
|
|
881
|
+
public async releaseLease(lease: ILeaseRecord): Promise<void> {
|
|
882
|
+
await this.releaseLeaseRecord(lease);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
private async startInternal(): Promise<void> {
|
|
886
|
+
if (this.options.environment.platform !== 'linux') {
|
|
887
|
+
throw new BrowserRuntimeError('FENCED', 'browser runtime requires Linux');
|
|
888
|
+
}
|
|
889
|
+
if (this.options.environment.uid === undefined || this.options.environment.uid === 0) {
|
|
890
|
+
throw new BrowserRuntimeError('FENCED', 'browser runtime requires a non-root uid');
|
|
891
|
+
}
|
|
892
|
+
await plugins.fsPromises.mkdir(this.options.runtimeDirectory, { recursive: true, mode: 0o700 });
|
|
893
|
+
await plugins.fsPromises.chmod(this.options.runtimeDirectory, 0o700);
|
|
894
|
+
if (!this.lockHandle) {
|
|
895
|
+
try {
|
|
896
|
+
this.lockHandle = await plugins.fsPromises.open(this.lockPath, 'wx', 0o600);
|
|
897
|
+
this.lockHandleClosed = false;
|
|
898
|
+
await this.lockHandle.chmod(0o600);
|
|
899
|
+
} catch (error) {
|
|
900
|
+
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
|
|
901
|
+
throw new BrowserRuntimeError('LOCKED');
|
|
902
|
+
}
|
|
903
|
+
throw error;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
await plugins.fsPromises.mkdir(this.profileRoot, { recursive: true, mode: 0o700 });
|
|
907
|
+
await plugins.fsPromises.chmod(this.profileRoot, 0o700);
|
|
908
|
+
const profileEntries = await plugins.fsPromises.readdir(this.profileRoot);
|
|
909
|
+
if (profileEntries.length > 0) {
|
|
910
|
+
throw new BrowserRuntimeError('FENCED', 'profile root is not empty');
|
|
911
|
+
}
|
|
912
|
+
await this.artifactStore.start();
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
private async stopInternal(): Promise<void> {
|
|
916
|
+
const peerResults = await Promise.allSettled(
|
|
917
|
+
[...this.framedPeers].map((peer) => peer.close()),
|
|
918
|
+
);
|
|
919
|
+
const revocationRecords = new Set([
|
|
920
|
+
...this.capabilitiesById.values(),
|
|
921
|
+
...this.failedRevocations,
|
|
922
|
+
]);
|
|
923
|
+
const revokeResults = await Promise.allSettled(
|
|
924
|
+
[...revocationRecords].map((record) => this.revokeCapabilityRecord(record)),
|
|
925
|
+
);
|
|
926
|
+
const retirementResults = await Promise.allSettled(
|
|
927
|
+
[...this.slots.keys()].map((projectId) => this.retireSlot(projectId, true)),
|
|
928
|
+
);
|
|
929
|
+
while (this.cleanupOperations.size > 0) {
|
|
930
|
+
await Promise.allSettled([...this.cleanupOperations]);
|
|
931
|
+
}
|
|
932
|
+
const errors = [...peerResults, ...revokeResults, ...retirementResults]
|
|
933
|
+
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
934
|
+
.map((result) => result.reason);
|
|
935
|
+
if (errors.length > 0) {
|
|
936
|
+
throw new AggregateError(errors, 'Browser runtime cleanup is incomplete');
|
|
937
|
+
}
|
|
938
|
+
await this.artifactStore.stop();
|
|
939
|
+
if (this.lockHandle) {
|
|
940
|
+
if (!this.lockHandleClosed) {
|
|
941
|
+
await this.lockHandle.close();
|
|
942
|
+
this.lockHandleClosed = true;
|
|
943
|
+
}
|
|
944
|
+
await plugins.fsPromises.rm(this.lockPath, { force: true });
|
|
945
|
+
this.lockHandle = undefined;
|
|
946
|
+
this.lockHandleClosed = false;
|
|
947
|
+
}
|
|
948
|
+
this.startPromise = undefined;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
private requireRunning(): void {
|
|
952
|
+
if (this.lifecycleState !== 'running') throw new BrowserRuntimeError('NOT_RUNNING');
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
private getOrCreateSlot(projectId: string): IProjectSlot {
|
|
956
|
+
let slot = this.slots.get(projectId);
|
|
957
|
+
if (slot) return slot;
|
|
958
|
+
if (this.slots.size >= this.options.maxProjectSlots) {
|
|
959
|
+
throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
960
|
+
}
|
|
961
|
+
slot = {
|
|
962
|
+
projectId,
|
|
963
|
+
mutex: new TransitionMutex(),
|
|
964
|
+
generation: 0,
|
|
965
|
+
permanentlyFenced: false,
|
|
966
|
+
};
|
|
967
|
+
this.slots.set(projectId, slot);
|
|
968
|
+
return slot;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
private async acquireAgentLease(
|
|
972
|
+
slot: IProjectSlot,
|
|
973
|
+
capability: ICapabilityRecord,
|
|
974
|
+
digest: plugins.Buffer,
|
|
975
|
+
acquisition: IAcquireBrowserRuntimeLeaseOptions,
|
|
976
|
+
): Promise<BrowserRuntimeLease> {
|
|
977
|
+
const release = slot.mutex.tryAcquire();
|
|
978
|
+
if (!release) throw new BrowserRuntimeError('BUSY');
|
|
979
|
+
try {
|
|
980
|
+
this.assertSlotAvailable(slot);
|
|
981
|
+
this.resolveCapability(
|
|
982
|
+
digest,
|
|
983
|
+
capability.peerId,
|
|
984
|
+
acquisition.expectedRole ?? 'agent',
|
|
985
|
+
acquisition.expectedSource,
|
|
986
|
+
acquisition.scopeId,
|
|
987
|
+
acquisition.sessionId,
|
|
988
|
+
);
|
|
989
|
+
if (slot.lease || slot.operation) throw new BrowserRuntimeError('BUSY');
|
|
990
|
+
await this.ensureSession(slot, acquisition.signal);
|
|
991
|
+
this.throwIfAcquisitionAborted(acquisition.signal);
|
|
992
|
+
this.resolveCapability(
|
|
993
|
+
digest,
|
|
994
|
+
capability.peerId,
|
|
995
|
+
acquisition.expectedRole ?? 'agent',
|
|
996
|
+
acquisition.expectedSource,
|
|
997
|
+
acquisition.scopeId,
|
|
998
|
+
acquisition.sessionId,
|
|
999
|
+
);
|
|
1000
|
+
const lease = this.createLeaseRecord(slot, capability, digest);
|
|
1001
|
+
return new BrowserRuntimeLease(this, lease);
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
if (slot.session && !slot.lease) this.scheduleIdleRetirement(slot);
|
|
1004
|
+
throw error;
|
|
1005
|
+
} finally {
|
|
1006
|
+
release();
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
private async acquireHumanLease(
|
|
1011
|
+
slot: IProjectSlot,
|
|
1012
|
+
capability: ICapabilityRecord,
|
|
1013
|
+
digest: plugins.Buffer,
|
|
1014
|
+
acquisition: IAcquireBrowserRuntimeLeaseOptions,
|
|
1015
|
+
): Promise<BrowserRuntimeLease> {
|
|
1016
|
+
const release = await slot.mutex.acquire();
|
|
1017
|
+
let generation = 0;
|
|
1018
|
+
let preemptedLease: ILeaseRecord | undefined;
|
|
1019
|
+
try {
|
|
1020
|
+
this.assertSlotAvailable(slot);
|
|
1021
|
+
this.resolveCapability(digest, capability.peerId, 'human', 'human');
|
|
1022
|
+
this.throwIfAcquisitionAborted(acquisition.signal);
|
|
1023
|
+
if (slot.fencingGeneration !== undefined) throw new BrowserRuntimeError('BUSY');
|
|
1024
|
+
if (slot.lease?.role === 'human') throw new BrowserRuntimeError('BUSY');
|
|
1025
|
+
generation = ++slot.generation;
|
|
1026
|
+
slot.fencingGeneration = generation;
|
|
1027
|
+
preemptedLease = slot.lease;
|
|
1028
|
+
if (preemptedLease) {
|
|
1029
|
+
preemptedLease.released = true;
|
|
1030
|
+
preemptedLease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1031
|
+
slot.operation?.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1032
|
+
}
|
|
1033
|
+
} finally {
|
|
1034
|
+
release();
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
try {
|
|
1038
|
+
if (preemptedLease) await this.quiesceSlot(slot);
|
|
1039
|
+
this.throwIfAcquisitionAborted(acquisition.signal);
|
|
1040
|
+
const finalRelease = await slot.mutex.acquire();
|
|
1041
|
+
try {
|
|
1042
|
+
if (slot.generation !== generation || slot.fencingGeneration !== generation) {
|
|
1043
|
+
throw new BrowserRuntimeError('BUSY');
|
|
1044
|
+
}
|
|
1045
|
+
if (preemptedLease) {
|
|
1046
|
+
if (preemptedLease.capability.lease === preemptedLease) {
|
|
1047
|
+
preemptedLease.capability.lease = undefined;
|
|
1048
|
+
}
|
|
1049
|
+
if (slot.lease === preemptedLease) slot.lease = undefined;
|
|
1050
|
+
}
|
|
1051
|
+
if (slot.permanentlyFenced) throw new BrowserRuntimeError('FENCED');
|
|
1052
|
+
slot.fencingGeneration = undefined;
|
|
1053
|
+
await this.ensureSession(slot, acquisition.signal);
|
|
1054
|
+
this.throwIfAcquisitionAborted(acquisition.signal);
|
|
1055
|
+
this.resolveCapability(digest, capability.peerId, 'human', 'human');
|
|
1056
|
+
const lease = this.createLeaseRecord(slot, capability, digest);
|
|
1057
|
+
return new BrowserRuntimeLease(this, lease);
|
|
1058
|
+
} finally {
|
|
1059
|
+
finalRelease();
|
|
1060
|
+
}
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
const cleanupRelease = await slot.mutex.acquire();
|
|
1063
|
+
try {
|
|
1064
|
+
if (preemptedLease) {
|
|
1065
|
+
if (preemptedLease.capability.lease === preemptedLease) {
|
|
1066
|
+
preemptedLease.capability.lease = undefined;
|
|
1067
|
+
}
|
|
1068
|
+
if (slot.lease === preemptedLease) slot.lease = undefined;
|
|
1069
|
+
}
|
|
1070
|
+
if (slot.fencingGeneration === generation) slot.fencingGeneration = undefined;
|
|
1071
|
+
if (slot.session && !slot.lease) this.scheduleIdleRetirement(slot);
|
|
1072
|
+
} finally {
|
|
1073
|
+
cleanupRelease();
|
|
1074
|
+
}
|
|
1075
|
+
throw error;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
private createLeaseRecord(
|
|
1080
|
+
slot: IProjectSlot,
|
|
1081
|
+
capability: ICapabilityRecord,
|
|
1082
|
+
digest: plugins.Buffer,
|
|
1083
|
+
): ILeaseRecord {
|
|
1084
|
+
if (capability.lease || slot.lease) throw new BrowserRuntimeError('BUSY');
|
|
1085
|
+
if (slot.idleTimer) {
|
|
1086
|
+
clearTimeout(slot.idleTimer);
|
|
1087
|
+
slot.idleTimer = undefined;
|
|
1088
|
+
}
|
|
1089
|
+
const lease: ILeaseRecord = {
|
|
1090
|
+
leaseId: randomId(18),
|
|
1091
|
+
capability,
|
|
1092
|
+
digest,
|
|
1093
|
+
peerId: capability.peerId,
|
|
1094
|
+
role: capability.role,
|
|
1095
|
+
slot,
|
|
1096
|
+
controller: new AbortController(),
|
|
1097
|
+
released: false,
|
|
1098
|
+
};
|
|
1099
|
+
slot.lease = lease;
|
|
1100
|
+
capability.lease = lease;
|
|
1101
|
+
return lease;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
private async ensureSession(slot: IProjectSlot, signalArg?: AbortSignal): Promise<void> {
|
|
1105
|
+
if (slot.session) return;
|
|
1106
|
+
this.assertSlotAvailable(slot);
|
|
1107
|
+
const profileDirectory = plugins.path.join(this.profileRoot, randomId(24));
|
|
1108
|
+
let profileCreated = false;
|
|
1109
|
+
let proxy: BrowserEgressProxy | undefined;
|
|
1110
|
+
let session: ILiveBrowserSessionLike | undefined;
|
|
1111
|
+
let unsubscribe: (() => void) | undefined;
|
|
1112
|
+
try {
|
|
1113
|
+
await plugins.fsPromises.mkdir(profileDirectory, { mode: 0o700 });
|
|
1114
|
+
profileCreated = true;
|
|
1115
|
+
await plugins.fsPromises.chmod(profileDirectory, 0o700);
|
|
1116
|
+
proxy = new BrowserEgressProxy({
|
|
1117
|
+
projectId: slot.projectId,
|
|
1118
|
+
...this.options.egress,
|
|
1119
|
+
});
|
|
1120
|
+
await proxy.start();
|
|
1121
|
+
const credentials = proxy.credentials;
|
|
1122
|
+
session = this.options.sessionFactory({
|
|
1123
|
+
requireSandbox: true,
|
|
1124
|
+
forceNoSandbox: false,
|
|
1125
|
+
usePipe: true,
|
|
1126
|
+
allowEvaluation: true,
|
|
1127
|
+
launchOptions: {
|
|
1128
|
+
headless: true,
|
|
1129
|
+
userDataDir: profileDirectory,
|
|
1130
|
+
args: [
|
|
1131
|
+
`--proxy-server=http://127.0.0.1:${proxy.port}`,
|
|
1132
|
+
'--proxy-bypass-list=<-loopback>',
|
|
1133
|
+
'--disable-quic',
|
|
1134
|
+
'--disable-dns-prefetch',
|
|
1135
|
+
'--disable-background-networking',
|
|
1136
|
+
'--disable-extensions',
|
|
1137
|
+
'--webrtc-ip-handling-policy=disable_non_proxied_udp',
|
|
1138
|
+
'--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE 127.0.0.1',
|
|
1139
|
+
],
|
|
1140
|
+
},
|
|
1141
|
+
security: {
|
|
1142
|
+
denyDownloads: true,
|
|
1143
|
+
denyFileChoosers: true,
|
|
1144
|
+
denyPermissions: true,
|
|
1145
|
+
httpNavigationOnly: true,
|
|
1146
|
+
proxyCredentials: credentials,
|
|
1147
|
+
},
|
|
1148
|
+
});
|
|
1149
|
+
unsubscribe = session.onEvent((event) => {
|
|
1150
|
+
this.handleSessionEvent(slot, session!, event);
|
|
1151
|
+
});
|
|
1152
|
+
const timeoutController = new AbortController();
|
|
1153
|
+
const timeout = setTimeout(() => {
|
|
1154
|
+
timeoutController.abort(new BrowserRuntimeError('CONFINEMENT_FAILED'));
|
|
1155
|
+
}, this.options.confinementProbeTimeoutMs);
|
|
1156
|
+
timeout.unref();
|
|
1157
|
+
const probeSignal = AbortSignal.any([
|
|
1158
|
+
this.lifecycleController.signal,
|
|
1159
|
+
signalArg ?? new AbortController().signal,
|
|
1160
|
+
timeoutController.signal,
|
|
1161
|
+
]);
|
|
1162
|
+
const runBounded = async (operation: Promise<void>): Promise<void> => {
|
|
1163
|
+
if (probeSignal.aborted) throw probeSignal.reason;
|
|
1164
|
+
let onAbort!: () => void;
|
|
1165
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
1166
|
+
onAbort = () => reject(probeSignal.reason);
|
|
1167
|
+
probeSignal.addEventListener('abort', onAbort, { once: true });
|
|
1168
|
+
});
|
|
1169
|
+
try {
|
|
1170
|
+
await Promise.race([operation, aborted]);
|
|
1171
|
+
} finally {
|
|
1172
|
+
probeSignal.removeEventListener('abort', onAbort);
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
try {
|
|
1176
|
+
await runBounded(session.start({ signal: probeSignal }));
|
|
1177
|
+
await runBounded(this.options.confinementProbe({ session, proxy, signal: probeSignal }));
|
|
1178
|
+
} finally {
|
|
1179
|
+
clearTimeout(timeout);
|
|
1180
|
+
}
|
|
1181
|
+
if (probeSignal.aborted) throw probeSignal.reason;
|
|
1182
|
+
slot.session = session;
|
|
1183
|
+
slot.proxy = proxy;
|
|
1184
|
+
slot.profileDirectory = profileDirectory;
|
|
1185
|
+
slot.unsubscribeSession = unsubscribe;
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
unsubscribe?.();
|
|
1188
|
+
let confirmed = true;
|
|
1189
|
+
if (session) {
|
|
1190
|
+
try {
|
|
1191
|
+
const termination = await waitBounded(
|
|
1192
|
+
session.terminate({
|
|
1193
|
+
gracefulTimeoutMs: this.options.terminationGraceMs,
|
|
1194
|
+
forceTimeoutMs: this.options.terminationForceMs,
|
|
1195
|
+
}),
|
|
1196
|
+
this.options.terminationGraceMs + this.options.terminationForceMs + 1000,
|
|
1197
|
+
);
|
|
1198
|
+
confirmed = termination.settled && termination.value.confirmedDead;
|
|
1199
|
+
} catch {
|
|
1200
|
+
confirmed = false;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (!confirmed) {
|
|
1204
|
+
slot.permanentlyFenced = true;
|
|
1205
|
+
slot.session = session;
|
|
1206
|
+
slot.proxy = proxy;
|
|
1207
|
+
slot.profileDirectory = profileDirectory;
|
|
1208
|
+
slot.unsubscribeSession = unsubscribe;
|
|
1209
|
+
throw new BrowserRuntimeError('FENCED');
|
|
1210
|
+
}
|
|
1211
|
+
let proxyStopped = true;
|
|
1212
|
+
let profileRemoved = !profileCreated;
|
|
1213
|
+
if (proxy) {
|
|
1214
|
+
await this.stopProxyBounded(proxy).catch(() => { proxyStopped = false; });
|
|
1215
|
+
}
|
|
1216
|
+
if (profileCreated) {
|
|
1217
|
+
await plugins.fsPromises.rm(profileDirectory, { recursive: true, force: true })
|
|
1218
|
+
.then(() => { profileRemoved = true; })
|
|
1219
|
+
.catch(() => undefined);
|
|
1220
|
+
}
|
|
1221
|
+
if (!proxyStopped || !profileRemoved) {
|
|
1222
|
+
slot.permanentlyFenced = true;
|
|
1223
|
+
if (!proxyStopped) slot.proxy = proxy;
|
|
1224
|
+
if (!profileRemoved) slot.profileDirectory = profileDirectory;
|
|
1225
|
+
throw new BrowserRuntimeError('FENCED');
|
|
1226
|
+
}
|
|
1227
|
+
if (signalArg?.aborted) throw new BrowserRuntimeError('ABORTED');
|
|
1228
|
+
throw error instanceof BrowserRuntimeError
|
|
1229
|
+
? error
|
|
1230
|
+
: new BrowserRuntimeError('CONFINEMENT_FAILED');
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
private async runOperation<T>(
|
|
1235
|
+
lease: ILeaseRecord,
|
|
1236
|
+
action: string,
|
|
1237
|
+
operationOptions: IBrowserRuntimeOperationOptions,
|
|
1238
|
+
execute: (signal: AbortSignal, session: ILiveBrowserSessionLike) => Promise<T>,
|
|
1239
|
+
): Promise<T> {
|
|
1240
|
+
if (!operationOptions || typeof operationOptions !== 'object') {
|
|
1241
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1242
|
+
}
|
|
1243
|
+
if (
|
|
1244
|
+
operationOptions.signal !== undefined
|
|
1245
|
+
&& !(operationOptions.signal instanceof AbortSignal)
|
|
1246
|
+
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1247
|
+
if (
|
|
1248
|
+
operationOptions.onOperationStarted !== undefined
|
|
1249
|
+
&& typeof operationOptions.onOperationStarted !== 'function'
|
|
1250
|
+
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1251
|
+
operationOptions.signal?.throwIfAborted();
|
|
1252
|
+
const slot = lease.slot;
|
|
1253
|
+
const release = slot.mutex.tryAcquire();
|
|
1254
|
+
if (!release) throw new BrowserRuntimeError('BUSY');
|
|
1255
|
+
let operation: IOperationRecord;
|
|
1256
|
+
let executionPromise: Promise<T>;
|
|
1257
|
+
let combinedSignal: AbortSignal;
|
|
1258
|
+
let timeout: ReturnType<typeof setTimeout>;
|
|
1259
|
+
let session: ILiveBrowserSessionLike;
|
|
1260
|
+
let generation: number;
|
|
1261
|
+
const startedAt = Date.now();
|
|
1262
|
+
try {
|
|
1263
|
+
this.requireValidLease(lease);
|
|
1264
|
+
this.assertSlotAvailable(slot);
|
|
1265
|
+
if (slot.operation) throw new BrowserRuntimeError('BUSY');
|
|
1266
|
+
session = slot.session!;
|
|
1267
|
+
generation = slot.generation;
|
|
1268
|
+
const timeoutMs = operationOptions.timeoutMs === undefined
|
|
1269
|
+
? this.options.operationTimeoutMs
|
|
1270
|
+
: validateInteger(operationOptions.timeoutMs, 'timeoutMs', 100, 120_000);
|
|
1271
|
+
const controller = new AbortController();
|
|
1272
|
+
const timeoutController = new AbortController();
|
|
1273
|
+
timeout = setTimeout(() => {
|
|
1274
|
+
timeoutController.abort(new BrowserRuntimeError('TIMEOUT'));
|
|
1275
|
+
}, timeoutMs);
|
|
1276
|
+
timeout.unref();
|
|
1277
|
+
combinedSignal = AbortSignal.any([
|
|
1278
|
+
controller.signal,
|
|
1279
|
+
lease.controller.signal,
|
|
1280
|
+
operationOptions.signal ?? new AbortController().signal,
|
|
1281
|
+
timeoutController.signal,
|
|
1282
|
+
]);
|
|
1283
|
+
executionPromise = Promise.resolve().then(() => execute(combinedSignal, session));
|
|
1284
|
+
operation = {
|
|
1285
|
+
operationId: randomId(18),
|
|
1286
|
+
lease,
|
|
1287
|
+
controller,
|
|
1288
|
+
action,
|
|
1289
|
+
startedAt,
|
|
1290
|
+
promise: executionPromise.then(() => undefined, () => undefined),
|
|
1291
|
+
};
|
|
1292
|
+
slot.operation = operation;
|
|
1293
|
+
try {
|
|
1294
|
+
operationOptions.onOperationStarted?.(operation.operationId);
|
|
1295
|
+
} catch {
|
|
1296
|
+
controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1297
|
+
}
|
|
1298
|
+
} finally {
|
|
1299
|
+
release();
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
let phase: 'completed' | 'failed' = 'completed';
|
|
1303
|
+
let errorCode: string | undefined;
|
|
1304
|
+
try {
|
|
1305
|
+
let onAbort!: () => void;
|
|
1306
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
1307
|
+
onAbort = () => reject(combinedSignal.reason ?? new BrowserRuntimeError('ABORTED'));
|
|
1308
|
+
combinedSignal.addEventListener('abort', onAbort, { once: true });
|
|
1309
|
+
});
|
|
1310
|
+
let result: T;
|
|
1311
|
+
try {
|
|
1312
|
+
result = await Promise.race([executionPromise!, aborted]);
|
|
1313
|
+
} finally {
|
|
1314
|
+
combinedSignal.removeEventListener('abort', onAbort);
|
|
1315
|
+
}
|
|
1316
|
+
if (combinedSignal.aborted) throw combinedSignal.reason;
|
|
1317
|
+
this.requireValidLease(lease);
|
|
1318
|
+
if (slot.session !== session! || slot.generation !== generation!) {
|
|
1319
|
+
throw new BrowserRuntimeError('ABORTED');
|
|
1320
|
+
}
|
|
1321
|
+
return result;
|
|
1322
|
+
} catch (error) {
|
|
1323
|
+
phase = 'failed';
|
|
1324
|
+
errorCode = error instanceof BrowserRuntimeError
|
|
1325
|
+
? error.code
|
|
1326
|
+
: error instanceof Error
|
|
1327
|
+
? truncateString(error.name, 64)
|
|
1328
|
+
: 'Error';
|
|
1329
|
+
if (combinedSignal!.aborted) {
|
|
1330
|
+
this.trackCleanup(this.enforceOperationQuiescence(operation!));
|
|
1331
|
+
const reason = combinedSignal!.reason;
|
|
1332
|
+
throw reason instanceof BrowserRuntimeError
|
|
1333
|
+
? reason
|
|
1334
|
+
: new BrowserRuntimeError(
|
|
1335
|
+
reason instanceof Error && reason.name === 'TimeoutError' ? 'TIMEOUT' : 'ABORTED',
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
throw error;
|
|
1339
|
+
} finally {
|
|
1340
|
+
clearTimeout(timeout!);
|
|
1341
|
+
executionPromise!.finally(async () => {
|
|
1342
|
+
const cleanupRelease = await slot.mutex.acquire();
|
|
1343
|
+
try {
|
|
1344
|
+
if (slot.operation === operation!) slot.operation = undefined;
|
|
1345
|
+
} finally {
|
|
1346
|
+
cleanupRelease();
|
|
1347
|
+
}
|
|
1348
|
+
}).catch(() => undefined);
|
|
1349
|
+
await this.emitAudit({
|
|
1350
|
+
operationId: operation!.operationId,
|
|
1351
|
+
capabilityId: lease.capability.capabilityId,
|
|
1352
|
+
projectId: lease.capability.projectId,
|
|
1353
|
+
actorId: lease.capability.actorId,
|
|
1354
|
+
role: lease.role,
|
|
1355
|
+
source: lease.capability.source,
|
|
1356
|
+
action,
|
|
1357
|
+
phase,
|
|
1358
|
+
startedAt,
|
|
1359
|
+
finishedAt: Date.now(),
|
|
1360
|
+
...(errorCode ? { errorCode } : {}),
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
private async executeAgentActionInternal(
|
|
1366
|
+
lease: ILeaseRecord,
|
|
1367
|
+
session: ILiveBrowserSessionLike,
|
|
1368
|
+
action: TBrowserAgentAction,
|
|
1369
|
+
signal: AbortSignal,
|
|
1370
|
+
): Promise<TBrowserAgentActionResult> {
|
|
1371
|
+
if (action.action === 'navigate') {
|
|
1372
|
+
await session.navigate({
|
|
1373
|
+
url: action.url,
|
|
1374
|
+
...(action.tabId ? { tabId: action.tabId } : {}),
|
|
1375
|
+
...(action.timeoutMs ? { timeoutMs: action.timeoutMs } : {}),
|
|
1376
|
+
}, { signal });
|
|
1377
|
+
return {
|
|
1378
|
+
action: 'navigate',
|
|
1379
|
+
state: this.projectState(session.getState()),
|
|
1380
|
+
} satisfies IBrowserActionStateResult;
|
|
1381
|
+
}
|
|
1382
|
+
if (action.action === 'snapshot') {
|
|
1383
|
+
const observation = await session.observe({
|
|
1384
|
+
...(action.tabId ? { tabId: action.tabId } : {}),
|
|
1385
|
+
...(action.maxCharacters ? { maxCharacters: action.maxCharacters } : {}),
|
|
1386
|
+
}, { signal });
|
|
1387
|
+
return {
|
|
1388
|
+
action: 'snapshot',
|
|
1389
|
+
tabId: truncateString(observation.tabId, 128),
|
|
1390
|
+
url: truncateString(observation.url, 8192),
|
|
1391
|
+
title: truncateString(observation.title, 2048),
|
|
1392
|
+
text: truncateString(observation.text, 50_000),
|
|
1393
|
+
truncated: observation.truncated,
|
|
1394
|
+
state: this.projectState(observation.state),
|
|
1395
|
+
} satisfies IBrowserObservationResult;
|
|
1396
|
+
}
|
|
1397
|
+
if (action.action === 'screenshot') {
|
|
1398
|
+
const snapshot = await session.captureSnapshot({
|
|
1399
|
+
...(action.tabId ? { tabId: action.tabId } : {}),
|
|
1400
|
+
...(action.format ? { format: action.format } : {}),
|
|
1401
|
+
...(action.quality !== undefined ? { quality: action.quality } : {}),
|
|
1402
|
+
}, { signal });
|
|
1403
|
+
try {
|
|
1404
|
+
signal.throwIfAborted();
|
|
1405
|
+
this.requireValidLease(lease);
|
|
1406
|
+
if (lease.slot.session !== session) throw new BrowserRuntimeError('ABORTED');
|
|
1407
|
+
const artifact = await this.artifactStore.store(
|
|
1408
|
+
lease.capability.projectId,
|
|
1409
|
+
snapshot.mimeType,
|
|
1410
|
+
snapshot.data,
|
|
1411
|
+
);
|
|
1412
|
+
return { action: 'screenshot', artifact } satisfies IBrowserScreenshotResult;
|
|
1413
|
+
} finally {
|
|
1414
|
+
snapshot.data.fill(0);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
const target = this.resolveSemanticTarget(session.getState(), action.tabId);
|
|
1418
|
+
if (action.action === 'click') {
|
|
1419
|
+
await session.click({
|
|
1420
|
+
...target,
|
|
1421
|
+
selector: action.selector,
|
|
1422
|
+
...(action.timeoutMs ? { timeoutMs: action.timeoutMs } : {}),
|
|
1423
|
+
}, { signal });
|
|
1424
|
+
} else if (action.action === 'fill') {
|
|
1425
|
+
await session.fill({
|
|
1426
|
+
...target,
|
|
1427
|
+
selector: action.selector,
|
|
1428
|
+
text: action.text,
|
|
1429
|
+
...(action.timeoutMs ? { timeoutMs: action.timeoutMs } : {}),
|
|
1430
|
+
}, { signal });
|
|
1431
|
+
} else {
|
|
1432
|
+
await session.press({
|
|
1433
|
+
...target,
|
|
1434
|
+
selector: action.selector,
|
|
1435
|
+
key: action.key,
|
|
1436
|
+
...(action.timeoutMs ? { timeoutMs: action.timeoutMs } : {}),
|
|
1437
|
+
}, { signal });
|
|
1438
|
+
}
|
|
1439
|
+
return {
|
|
1440
|
+
action: action.action,
|
|
1441
|
+
state: this.projectState(session.getState()),
|
|
1442
|
+
} satisfies IBrowserActionStateResult;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
private resolveSemanticTarget(
|
|
1446
|
+
state: plugins.smartpuppeteer.ILiveBrowserState,
|
|
1447
|
+
tabIdArg?: string,
|
|
1448
|
+
): { tabId: string; generation: number; viewportRevision: number } {
|
|
1449
|
+
const tabId = tabIdArg ?? state.activeTabId;
|
|
1450
|
+
const tab = state.tabs.find((candidate) => candidate.id === tabId);
|
|
1451
|
+
if (!tab) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1452
|
+
return {
|
|
1453
|
+
tabId: tab.id,
|
|
1454
|
+
generation: tab.generation,
|
|
1455
|
+
viewportRevision: state.viewportRevision,
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
private projectState(state: plugins.smartpuppeteer.ILiveBrowserState): IBrowserRuntimeState {
|
|
1460
|
+
return {
|
|
1461
|
+
status: state.status,
|
|
1462
|
+
activeTabId: state.activeTabId ? truncateString(state.activeTabId, 128) : null,
|
|
1463
|
+
viewportRevision: state.viewportRevision,
|
|
1464
|
+
viewport: {
|
|
1465
|
+
width: state.viewport.width,
|
|
1466
|
+
height: state.viewport.height,
|
|
1467
|
+
deviceScaleFactor: state.viewport.deviceScaleFactor,
|
|
1468
|
+
},
|
|
1469
|
+
tabs: state.tabs.slice(0, 256).map((tab) => ({
|
|
1470
|
+
id: truncateString(tab.id, 128),
|
|
1471
|
+
url: truncateString(tab.url, 8192),
|
|
1472
|
+
title: truncateString(tab.title, 2048),
|
|
1473
|
+
active: tab.active,
|
|
1474
|
+
status: tab.status,
|
|
1475
|
+
generation: tab.generation,
|
|
1476
|
+
appliedViewportRevision: tab.appliedViewportRevision,
|
|
1477
|
+
streaming: tab.streaming,
|
|
1478
|
+
})),
|
|
1479
|
+
...(state.lastError
|
|
1480
|
+
? {
|
|
1481
|
+
lastError: {
|
|
1482
|
+
code: truncateString(state.lastError.code, 128),
|
|
1483
|
+
fatal: state.lastError.fatal,
|
|
1484
|
+
...(state.lastError.tabId
|
|
1485
|
+
? { tabId: truncateString(state.lastError.tabId, 128) }
|
|
1486
|
+
: {}),
|
|
1487
|
+
},
|
|
1488
|
+
}
|
|
1489
|
+
: {}),
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
private handleSessionEvent(
|
|
1494
|
+
slot: IProjectSlot,
|
|
1495
|
+
session: ILiveBrowserSessionLike,
|
|
1496
|
+
event: plugins.smartpuppeteer.TLiveBrowserEvent,
|
|
1497
|
+
): void {
|
|
1498
|
+
if (slot.session && slot.session !== session) {
|
|
1499
|
+
if (event.type === 'frame') {
|
|
1500
|
+
this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, {
|
|
1501
|
+
tabId: event.frame.tabId,
|
|
1502
|
+
sequence: event.frame.sequence,
|
|
1503
|
+
generation: event.frame.generation,
|
|
1504
|
+
viewportRevision: event.frame.viewportRevision,
|
|
1505
|
+
}));
|
|
1506
|
+
}
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
const subscription = slot.frameSubscription;
|
|
1510
|
+
if (event.type === 'frame') {
|
|
1511
|
+
const acknowledgement = {
|
|
1512
|
+
tabId: event.frame.tabId,
|
|
1513
|
+
sequence: event.frame.sequence,
|
|
1514
|
+
generation: event.frame.generation,
|
|
1515
|
+
viewportRevision: event.frame.viewportRevision,
|
|
1516
|
+
};
|
|
1517
|
+
if (!subscription || subscription.closed) {
|
|
1518
|
+
this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
try {
|
|
1522
|
+
this.requireValidLease(subscription.lease);
|
|
1523
|
+
} catch {
|
|
1524
|
+
this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
|
|
1525
|
+
this.trackCleanup(this.revokeCapabilityRecord(subscription.lease.capability));
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
if (subscription.outstanding) {
|
|
1529
|
+
this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
if (event.frame.data.byteLength > this.options.maxFrameBytes) {
|
|
1533
|
+
this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
|
|
1534
|
+
this.pushToSubscription(subscription, {
|
|
1535
|
+
type: 'error',
|
|
1536
|
+
error: { code: 'FRAME_TOO_LARGE', fatal: true, tabId: event.frame.tabId },
|
|
1537
|
+
});
|
|
1538
|
+
this.trackCleanup(this.revokeCapabilityRecord(subscription.lease.capability));
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
const timer = setTimeout(() => {
|
|
1542
|
+
if (subscription.outstanding?.acknowledgement.sequence !== acknowledgement.sequence) return;
|
|
1543
|
+
subscription.outstanding = undefined;
|
|
1544
|
+
this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
|
|
1545
|
+
this.trackCleanup(this.revokeCapabilityRecord(subscription.lease.capability));
|
|
1546
|
+
}, this.options.frameAcknowledgementTimeoutMs);
|
|
1547
|
+
timer.unref();
|
|
1548
|
+
subscription.outstanding = { acknowledgement, timer };
|
|
1549
|
+
this.pushToSubscription(subscription, { type: 'frame', frame: event.frame });
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
if (event.type === 'error' && event.error.fatal && slot.lease) {
|
|
1553
|
+
this.trackCleanup(this.revokeCapabilityRecord(slot.lease.capability));
|
|
1554
|
+
}
|
|
1555
|
+
if (!subscription || subscription.closed) return;
|
|
1556
|
+
if (event.type === 'state') {
|
|
1557
|
+
this.pushToSubscription(subscription, { type: 'state', state: this.projectState(event.state) });
|
|
1558
|
+
} else {
|
|
1559
|
+
this.pushToSubscription(subscription, {
|
|
1560
|
+
type: 'error',
|
|
1561
|
+
error: {
|
|
1562
|
+
code: truncateString(event.error.code, 128),
|
|
1563
|
+
fatal: event.error.fatal,
|
|
1564
|
+
...(event.error.tabId ? { tabId: truncateString(event.error.tabId, 128) } : {}),
|
|
1565
|
+
},
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
private pushToSubscription(
|
|
1571
|
+
subscription: IFrameSubscriptionRecord,
|
|
1572
|
+
event: TBrowserRuntimeEvent,
|
|
1573
|
+
): void {
|
|
1574
|
+
try {
|
|
1575
|
+
this.requireValidLease(subscription.lease);
|
|
1576
|
+
subscription.listener(event);
|
|
1577
|
+
} catch {
|
|
1578
|
+
if (event.type === 'frame' && subscription.outstanding) {
|
|
1579
|
+
const outstanding = subscription.outstanding;
|
|
1580
|
+
clearTimeout(outstanding.timer);
|
|
1581
|
+
subscription.outstanding = undefined;
|
|
1582
|
+
this.trackCleanup(this.acknowledgeFrameInBackground(
|
|
1583
|
+
subscription.lease.slot,
|
|
1584
|
+
subscription.lease.slot.session!,
|
|
1585
|
+
outstanding.acknowledgement,
|
|
1586
|
+
));
|
|
1587
|
+
}
|
|
1588
|
+
this.trackCleanup(this.revokeCapabilityRecord(subscription.lease.capability));
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
private async closeFrameSubscription(subscription: IFrameSubscriptionRecord): Promise<void> {
|
|
1593
|
+
if (subscription.closed) return;
|
|
1594
|
+
subscription.closed = true;
|
|
1595
|
+
const slot = subscription.lease.slot;
|
|
1596
|
+
if (slot.frameSubscription === subscription) slot.frameSubscription = undefined;
|
|
1597
|
+
if (subscription.outstanding) {
|
|
1598
|
+
clearTimeout(subscription.outstanding.timer);
|
|
1599
|
+
const acknowledgement = subscription.outstanding.acknowledgement;
|
|
1600
|
+
subscription.outstanding = undefined;
|
|
1601
|
+
const session = slot.session;
|
|
1602
|
+
if (session) {
|
|
1603
|
+
const operation = session.acknowledgeFrame(acknowledgement);
|
|
1604
|
+
const result = await waitBounded(operation, this.options.frameAcknowledgementTimeoutMs)
|
|
1605
|
+
.catch(() => ({ settled: true as const, value: { accepted: false } }));
|
|
1606
|
+
if ((!result.settled || !result.value.accepted) && slot.session === session) {
|
|
1607
|
+
operation.catch(() => undefined);
|
|
1608
|
+
await this.terminateSlotSession(slot);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
private async acknowledgeFrameInBackground(
|
|
1615
|
+
slot: IProjectSlot,
|
|
1616
|
+
session: ILiveBrowserSessionLike,
|
|
1617
|
+
acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest,
|
|
1618
|
+
): Promise<void> {
|
|
1619
|
+
const operation = session.acknowledgeFrame(acknowledgement);
|
|
1620
|
+
const result = await waitBounded(operation, this.options.frameAcknowledgementTimeoutMs)
|
|
1621
|
+
.catch(() => ({ settled: true as const, value: { accepted: false } }));
|
|
1622
|
+
if (result.settled && result.value.accepted) return;
|
|
1623
|
+
operation.catch(() => undefined);
|
|
1624
|
+
if (slot.session !== session) return;
|
|
1625
|
+
if (slot.lease) await this.revokeCapabilityRecord(slot.lease.capability);
|
|
1626
|
+
else await this.retireSlot(slot.projectId, true);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
private requireValidLease(lease: ILeaseRecord): void {
|
|
1630
|
+
if (lease.released || lease.controller.signal.aborted) {
|
|
1631
|
+
throw new BrowserRuntimeError('CAPABILITY_REVOKED');
|
|
1632
|
+
}
|
|
1633
|
+
const capability = this.resolveCapability(
|
|
1634
|
+
lease.digest,
|
|
1635
|
+
lease.peerId,
|
|
1636
|
+
lease.role,
|
|
1637
|
+
lease.capability.source,
|
|
1638
|
+
);
|
|
1639
|
+
if (
|
|
1640
|
+
capability !== lease.capability
|
|
1641
|
+
|| capability.lease !== lease
|
|
1642
|
+
|| lease.slot.lease !== lease
|
|
1643
|
+
|| !lease.slot.session
|
|
1644
|
+
) {
|
|
1645
|
+
throw new BrowserRuntimeError('CAPABILITY_REVOKED');
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
private resolveCapability(
|
|
1650
|
+
digest: plugins.Buffer,
|
|
1651
|
+
peerId: string,
|
|
1652
|
+
expectedRole?: TBrowserActorRole,
|
|
1653
|
+
expectedSource?: TBrowserCapabilitySource,
|
|
1654
|
+
expectedScopeId?: string,
|
|
1655
|
+
expectedSessionId?: string,
|
|
1656
|
+
): ICapabilityRecord {
|
|
1657
|
+
const record = this.capabilitiesByDigest.get(digest.toString('hex'));
|
|
1658
|
+
if (!record || !digestsEqual(record.digest, digest) || record.peerId !== peerId) {
|
|
1659
|
+
throw new BrowserRuntimeError('CAPABILITY_INVALID');
|
|
1660
|
+
}
|
|
1661
|
+
if (record.expiresAt <= Date.now()) {
|
|
1662
|
+
record.state = 'expired';
|
|
1663
|
+
this.trackCleanup(this.revokeCapabilityRecord(record));
|
|
1664
|
+
throw new BrowserRuntimeError('CAPABILITY_EXPIRED');
|
|
1665
|
+
}
|
|
1666
|
+
if (record.state !== 'active') throw new BrowserRuntimeError('CAPABILITY_REVOKED');
|
|
1667
|
+
if (
|
|
1668
|
+
(expectedRole && record.role !== expectedRole)
|
|
1669
|
+
|| (expectedSource && record.source !== expectedSource)
|
|
1670
|
+
|| (expectedScopeId && record.scopeId !== expectedScopeId)
|
|
1671
|
+
|| (expectedSessionId && record.sessionId !== expectedSessionId)
|
|
1672
|
+
) {
|
|
1673
|
+
throw new BrowserRuntimeError('CAPABILITY_INVALID');
|
|
1674
|
+
}
|
|
1675
|
+
return record;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
private describeCapability(record: ICapabilityRecord): IBrowserCapabilityDescriptor {
|
|
1679
|
+
return {
|
|
1680
|
+
capabilityId: record.capabilityId,
|
|
1681
|
+
projectId: record.projectId,
|
|
1682
|
+
actorId: record.actorId,
|
|
1683
|
+
role: record.role,
|
|
1684
|
+
peerId: record.peerId,
|
|
1685
|
+
source: record.source,
|
|
1686
|
+
...(record.scopeId ? { scopeId: record.scopeId } : {}),
|
|
1687
|
+
...(record.sessionId ? { sessionId: record.sessionId } : {}),
|
|
1688
|
+
expiresAt: record.expiresAt,
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
private async revokeCapabilityRecord(
|
|
1693
|
+
capability: ICapabilityRecord,
|
|
1694
|
+
invokeDisconnect = true,
|
|
1695
|
+
): Promise<void> {
|
|
1696
|
+
if (capability.revocationComplete) return;
|
|
1697
|
+
if (capability.revocationPromise) return capability.revocationPromise;
|
|
1698
|
+
capability.revocationInvokeDisconnect ??= invokeDisconnect;
|
|
1699
|
+
if (capability.state === 'active') capability.state = 'revoked';
|
|
1700
|
+
clearTimeout(capability.expiryTimer);
|
|
1701
|
+
this.capabilitiesByDigest.delete(capability.digest.toString('hex'));
|
|
1702
|
+
const operation = (async () => {
|
|
1703
|
+
const errors: unknown[] = [];
|
|
1704
|
+
const lease = capability.lease;
|
|
1705
|
+
if (!capability.leaseCleanupComplete) {
|
|
1706
|
+
if (lease) {
|
|
1707
|
+
await this.releaseLeaseRecord(lease).then(
|
|
1708
|
+
() => { capability.leaseCleanupComplete = true; },
|
|
1709
|
+
(error) => errors.push(error),
|
|
1710
|
+
);
|
|
1711
|
+
} else {
|
|
1712
|
+
capability.leaseCleanupComplete = true;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
if (!capability.revocationInvokeDisconnect) capability.disconnectComplete = true;
|
|
1716
|
+
if (capability.revocationInvokeDisconnect && !capability.disconnectComplete) {
|
|
1717
|
+
if (capability.disconnect) {
|
|
1718
|
+
const disconnectController = new AbortController();
|
|
1719
|
+
let disconnectFailed = false;
|
|
1720
|
+
const result = await waitBounded(
|
|
1721
|
+
Promise.resolve().then(() => capability.disconnect!(disconnectController.signal)),
|
|
1722
|
+
this.options.quiescenceTimeoutMs,
|
|
1723
|
+
).catch((error) => {
|
|
1724
|
+
disconnectFailed = true;
|
|
1725
|
+
errors.push(error);
|
|
1726
|
+
return { settled: true as const, value: undefined };
|
|
1727
|
+
});
|
|
1728
|
+
if (!result.settled) {
|
|
1729
|
+
disconnectController.abort(new BrowserRuntimeError('TIMEOUT'));
|
|
1730
|
+
errors.push(new BrowserRuntimeError('TIMEOUT'));
|
|
1731
|
+
} else if (!disconnectFailed) {
|
|
1732
|
+
capability.disconnectComplete = true;
|
|
1733
|
+
}
|
|
1734
|
+
} else {
|
|
1735
|
+
const framedPeer = [...this.framedPeers].find((peer) => peer.peerId === capability.peerId);
|
|
1736
|
+
if (framedPeer) {
|
|
1737
|
+
await framedPeer.close().then(
|
|
1738
|
+
() => { capability.disconnectComplete = true; },
|
|
1739
|
+
(error) => errors.push(error),
|
|
1740
|
+
);
|
|
1741
|
+
} else {
|
|
1742
|
+
capability.disconnectComplete = true;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
if (errors.length > 0) throw new AggregateError(errors, 'Capability cleanup is incomplete');
|
|
1747
|
+
capability.revocationComplete = true;
|
|
1748
|
+
this.capabilitiesById.delete(capability.capabilityId);
|
|
1749
|
+
const slot = this.slots.get(capability.projectId);
|
|
1750
|
+
if (
|
|
1751
|
+
slot
|
|
1752
|
+
&& !slot.mutex.busy
|
|
1753
|
+
&& !slot.session
|
|
1754
|
+
&& !slot.lease
|
|
1755
|
+
&& !slot.operation
|
|
1756
|
+
&& ![...this.capabilitiesById.values()].some((record) => (
|
|
1757
|
+
record.projectId === capability.projectId
|
|
1758
|
+
))
|
|
1759
|
+
) this.slots.delete(capability.projectId);
|
|
1760
|
+
})();
|
|
1761
|
+
capability.revocationPromise = operation;
|
|
1762
|
+
try {
|
|
1763
|
+
await operation;
|
|
1764
|
+
this.failedRevocations.delete(capability);
|
|
1765
|
+
} catch (error) {
|
|
1766
|
+
this.failedRevocations.add(capability);
|
|
1767
|
+
throw error;
|
|
1768
|
+
} finally {
|
|
1769
|
+
if (capability.revocationPromise === operation) capability.revocationPromise = undefined;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
private async releaseLeaseRecord(lease: ILeaseRecord): Promise<void> {
|
|
1774
|
+
if (lease.releasePromise) return lease.releasePromise;
|
|
1775
|
+
const operation = this.releaseLeaseRecordInternal(lease);
|
|
1776
|
+
lease.releasePromise = operation;
|
|
1777
|
+
try {
|
|
1778
|
+
await operation;
|
|
1779
|
+
} finally {
|
|
1780
|
+
if (lease.releasePromise === operation) lease.releasePromise = undefined;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
private async releaseLeaseRecordInternal(lease: ILeaseRecord): Promise<void> {
|
|
1785
|
+
if (lease.released && lease.slot.lease !== lease) return;
|
|
1786
|
+
const slot = lease.slot;
|
|
1787
|
+
const release = await slot.mutex.acquire();
|
|
1788
|
+
const generation = lease.releaseGeneration ?? ++slot.generation;
|
|
1789
|
+
lease.releaseGeneration = generation;
|
|
1790
|
+
try {
|
|
1791
|
+
if (slot.fencingGeneration !== undefined && slot.fencingGeneration !== generation) {
|
|
1792
|
+
lease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1793
|
+
} else {
|
|
1794
|
+
slot.fencingGeneration = generation;
|
|
1795
|
+
}
|
|
1796
|
+
lease.released = true;
|
|
1797
|
+
lease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1798
|
+
if (slot.operation?.lease === lease) {
|
|
1799
|
+
slot.operation.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1800
|
+
}
|
|
1801
|
+
} finally {
|
|
1802
|
+
release();
|
|
1803
|
+
}
|
|
1804
|
+
await this.closeFrameSubscriptionIfLease(lease);
|
|
1805
|
+
await this.quiesceSlot(slot);
|
|
1806
|
+
if (
|
|
1807
|
+
slot.permanentlyFenced
|
|
1808
|
+
&& (slot.session || slot.proxy || slot.profileDirectory)
|
|
1809
|
+
) await this.terminateSlotSession(slot);
|
|
1810
|
+
const finalRelease = await slot.mutex.acquire();
|
|
1811
|
+
try {
|
|
1812
|
+
if (slot.lease === lease) slot.lease = undefined;
|
|
1813
|
+
if (lease.capability.lease === lease) lease.capability.lease = undefined;
|
|
1814
|
+
if (slot.fencingGeneration === generation) slot.fencingGeneration = undefined;
|
|
1815
|
+
lease.releaseGeneration = undefined;
|
|
1816
|
+
this.scheduleIdleRetirement(slot);
|
|
1817
|
+
} finally {
|
|
1818
|
+
finalRelease();
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
private async closeFrameSubscriptionIfLease(lease: ILeaseRecord): Promise<void> {
|
|
1823
|
+
const subscription = lease.slot.frameSubscription;
|
|
1824
|
+
if (subscription?.lease === lease) await this.closeFrameSubscription(subscription);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
private async quiesceSlot(slot: IProjectSlot): Promise<void> {
|
|
1828
|
+
const operation = slot.operation;
|
|
1829
|
+
if (!operation) return;
|
|
1830
|
+
operation.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1831
|
+
const settled = await waitBounded(operation.promise, this.options.quiescenceTimeoutMs);
|
|
1832
|
+
if (settled.settled) return;
|
|
1833
|
+
await this.terminateSlotSession(slot);
|
|
1834
|
+
if (slot.operation === operation) slot.operation = undefined;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
private async enforceOperationQuiescence(operation: IOperationRecord): Promise<void> {
|
|
1838
|
+
const settled = await waitBounded(operation.promise, this.options.quiescenceTimeoutMs);
|
|
1839
|
+
if (settled.settled || operation.lease.released) return;
|
|
1840
|
+
await this.revokeCapabilityRecord(operation.lease.capability);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
private async terminateSlotSession(slot: IProjectSlot): Promise<void> {
|
|
1844
|
+
const session = slot.session;
|
|
1845
|
+
if (session) {
|
|
1846
|
+
let termination: plugins.smartpuppeteer.ILiveBrowserTerminationResult;
|
|
1847
|
+
try {
|
|
1848
|
+
const result = await waitBounded(
|
|
1849
|
+
session.terminate({
|
|
1850
|
+
gracefulTimeoutMs: this.options.terminationGraceMs,
|
|
1851
|
+
forceTimeoutMs: this.options.terminationForceMs,
|
|
1852
|
+
}),
|
|
1853
|
+
this.options.terminationGraceMs + this.options.terminationForceMs + 1000,
|
|
1854
|
+
);
|
|
1855
|
+
if (!result.settled) throw new BrowserRuntimeError('TIMEOUT');
|
|
1856
|
+
termination = result.value;
|
|
1857
|
+
} catch {
|
|
1858
|
+
slot.permanentlyFenced = true;
|
|
1859
|
+
throw new BrowserRuntimeError('FENCED');
|
|
1860
|
+
}
|
|
1861
|
+
if (!termination.confirmedDead) {
|
|
1862
|
+
slot.permanentlyFenced = true;
|
|
1863
|
+
throw new BrowserRuntimeError('FENCED');
|
|
1864
|
+
}
|
|
1865
|
+
slot.unsubscribeSession?.();
|
|
1866
|
+
slot.unsubscribeSession = undefined;
|
|
1867
|
+
slot.session = undefined;
|
|
1868
|
+
}
|
|
1869
|
+
if (slot.proxy) {
|
|
1870
|
+
await this.stopProxyBounded(slot.proxy).then(
|
|
1871
|
+
() => { slot.proxy = undefined; },
|
|
1872
|
+
() => { slot.permanentlyFenced = true; },
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
if (slot.profileDirectory) {
|
|
1876
|
+
await plugins.fsPromises.rm(slot.profileDirectory, { recursive: true, force: true }).then(
|
|
1877
|
+
() => { slot.profileDirectory = undefined; },
|
|
1878
|
+
() => { slot.permanentlyFenced = true; },
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
if (slot.proxy || slot.profileDirectory) throw new BrowserRuntimeError('FENCED');
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
private scheduleIdleRetirement(slot: IProjectSlot): void {
|
|
1885
|
+
if (slot.idleTimer) clearTimeout(slot.idleTimer);
|
|
1886
|
+
const generation = slot.generation;
|
|
1887
|
+
slot.idleTimer = setTimeout(() => {
|
|
1888
|
+
if (slot.generation !== generation || slot.lease) return;
|
|
1889
|
+
this.trackCleanup(this.retireSlot(slot.projectId, false));
|
|
1890
|
+
}, this.options.idleRetirementMs);
|
|
1891
|
+
slot.idleTimer.unref();
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
private async stopProxyBounded(proxy: BrowserEgressProxy): Promise<void> {
|
|
1895
|
+
const operation = proxy.stop();
|
|
1896
|
+
const result = await waitBounded(
|
|
1897
|
+
operation,
|
|
1898
|
+
this.options.terminationGraceMs + this.options.terminationForceMs + 1000,
|
|
1899
|
+
);
|
|
1900
|
+
if (!result.settled) {
|
|
1901
|
+
operation.catch(() => undefined);
|
|
1902
|
+
throw new BrowserRuntimeError('TIMEOUT');
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
private async retireSlot(projectId: string, force: boolean): Promise<void> {
|
|
1907
|
+
const slot = this.slots.get(projectId);
|
|
1908
|
+
if (!slot) return;
|
|
1909
|
+
const release = await slot.mutex.acquire();
|
|
1910
|
+
const generation = ++slot.generation;
|
|
1911
|
+
try {
|
|
1912
|
+
if (!force && (slot.lease || slot.operation)) return;
|
|
1913
|
+
slot.fencingGeneration = generation;
|
|
1914
|
+
if (slot.idleTimer) {
|
|
1915
|
+
clearTimeout(slot.idleTimer);
|
|
1916
|
+
slot.idleTimer = undefined;
|
|
1917
|
+
}
|
|
1918
|
+
if (slot.lease) {
|
|
1919
|
+
slot.lease.released = true;
|
|
1920
|
+
slot.lease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1921
|
+
}
|
|
1922
|
+
slot.operation?.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1923
|
+
} finally {
|
|
1924
|
+
release();
|
|
1925
|
+
}
|
|
1926
|
+
await this.quiesceSlot(slot);
|
|
1927
|
+
if (slot.lease) await this.closeFrameSubscriptionIfLease(slot.lease);
|
|
1928
|
+
await this.terminateSlotSession(slot);
|
|
1929
|
+
await this.artifactStore.purgeProject(projectId);
|
|
1930
|
+
const finalRelease = await slot.mutex.acquire();
|
|
1931
|
+
try {
|
|
1932
|
+
if (slot.generation !== generation || slot.fencingGeneration !== generation) {
|
|
1933
|
+
throw new BrowserRuntimeError('BUSY');
|
|
1934
|
+
}
|
|
1935
|
+
const lease = slot.lease;
|
|
1936
|
+
if (lease && lease.capability.lease === lease) {
|
|
1937
|
+
lease.capability.lease = undefined;
|
|
1938
|
+
}
|
|
1939
|
+
slot.lease = undefined;
|
|
1940
|
+
slot.operation = undefined;
|
|
1941
|
+
slot.fencingGeneration = undefined;
|
|
1942
|
+
this.slots.delete(projectId);
|
|
1943
|
+
} finally {
|
|
1944
|
+
finalRelease();
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
private assertSlotAvailable(slot: IProjectSlot): void {
|
|
1949
|
+
if (slot.permanentlyFenced) throw new BrowserRuntimeError('FENCED');
|
|
1950
|
+
if (slot.fencingGeneration !== undefined) throw new BrowserRuntimeError('BUSY');
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
private throwIfAcquisitionAborted(signal?: AbortSignal): void {
|
|
1954
|
+
if (signal?.aborted) throw new BrowserRuntimeError('ABORTED');
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
private requireHuman(lease: ILeaseRecord): void {
|
|
1958
|
+
if (lease.role !== 'human') throw new BrowserRuntimeError('CAPABILITY_INVALID');
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
private validateRole(value: unknown): TBrowserActorRole {
|
|
1962
|
+
if (value !== 'human' && value !== 'agent') throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1963
|
+
return value;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
private validateSource(value: unknown): TBrowserCapabilitySource {
|
|
1967
|
+
if (value !== 'human' && value !== 'flex' && value !== 'mcp') {
|
|
1968
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1969
|
+
}
|
|
1970
|
+
return value;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
private trackCleanup(operation: Promise<void>): void {
|
|
1974
|
+
const observed = operation.catch(() => undefined).finally(() => {
|
|
1975
|
+
this.cleanupOperations.delete(observed);
|
|
1976
|
+
});
|
|
1977
|
+
this.cleanupOperations.add(observed);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
private async emitAudit(event: IBrowserRuntimeAuditEvent): Promise<void> {
|
|
1981
|
+
if (!this.options.audit) return;
|
|
1982
|
+
const controller = new AbortController();
|
|
1983
|
+
const operation = Promise.resolve().then(() => this.options.audit!(event, controller.signal));
|
|
1984
|
+
const observed = waitBounded(operation, this.options.auditTimeoutMs)
|
|
1985
|
+
.then((result) => {
|
|
1986
|
+
if (!result.settled) controller.abort(new BrowserRuntimeError('TIMEOUT'));
|
|
1987
|
+
})
|
|
1988
|
+
.catch(() => undefined);
|
|
1989
|
+
this.trackCleanup(observed);
|
|
1990
|
+
await observed;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
export class BrowserRuntimeLease {
|
|
1995
|
+
/** @internal */
|
|
1996
|
+
constructor(
|
|
1997
|
+
private readonly runtime: BrowserRuntime,
|
|
1998
|
+
private readonly record: ILeaseRecord,
|
|
1999
|
+
) {}
|
|
2000
|
+
|
|
2001
|
+
public get leaseId(): string {
|
|
2002
|
+
return this.record.leaseId;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
public get role(): TBrowserActorRole {
|
|
2006
|
+
return this.record.role;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
public get projectId(): string {
|
|
2010
|
+
return this.record.capability.projectId;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
public getState(): IBrowserRuntimeState {
|
|
2014
|
+
return this.runtime.getLeaseState(this.record);
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
public executeAgentAction(
|
|
2018
|
+
action: TBrowserAgentAction,
|
|
2019
|
+
options?: IBrowserRuntimeOperationOptions,
|
|
2020
|
+
): Promise<TBrowserAgentActionResult> {
|
|
2021
|
+
return this.runtime.executeLeaseAgentAction(this.record, action, options);
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
public executeHumanAction(
|
|
2025
|
+
action: TBrowserAgentAction,
|
|
2026
|
+
options?: IBrowserRuntimeOperationOptions,
|
|
2027
|
+
): Promise<TBrowserAgentActionResult> {
|
|
2028
|
+
return this.runtime.leaseHumanAgentAction(this.record, action, options);
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
public createTab(
|
|
2032
|
+
options?: plugins.smartpuppeteer.ILiveBrowserCreateTabOptions,
|
|
2033
|
+
operationOptions?: IBrowserRuntimeOperationOptions,
|
|
2034
|
+
): Promise<IBrowserRuntimeState> {
|
|
2035
|
+
return this.runtime.leaseCreateTab(this.record, options, operationOptions);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
public activateTab(
|
|
2039
|
+
tabId: string,
|
|
2040
|
+
operationOptions?: IBrowserRuntimeOperationOptions,
|
|
2041
|
+
): Promise<IBrowserRuntimeState> {
|
|
2042
|
+
return this.runtime.leaseActivateTab(this.record, tabId, operationOptions);
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
public closeTab(
|
|
2046
|
+
tabId: string,
|
|
2047
|
+
operationOptions?: IBrowserRuntimeOperationOptions,
|
|
2048
|
+
): Promise<IBrowserRuntimeState> {
|
|
2049
|
+
return this.runtime.leaseCloseTab(this.record, tabId, operationOptions);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
public back(
|
|
2053
|
+
options?: plugins.smartpuppeteer.ILiveBrowserNavigationOptions,
|
|
2054
|
+
operationOptions?: IBrowserRuntimeOperationOptions,
|
|
2055
|
+
): Promise<IBrowserRuntimeState> {
|
|
2056
|
+
return this.runtime.leaseHistory(this.record, 'back', options, operationOptions);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
public forward(
|
|
2060
|
+
options?: plugins.smartpuppeteer.ILiveBrowserNavigationOptions,
|
|
2061
|
+
operationOptions?: IBrowserRuntimeOperationOptions,
|
|
2062
|
+
): Promise<IBrowserRuntimeState> {
|
|
2063
|
+
return this.runtime.leaseHistory(this.record, 'forward', options, operationOptions);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
public reload(
|
|
2067
|
+
options?: plugins.smartpuppeteer.ILiveBrowserNavigationOptions,
|
|
2068
|
+
operationOptions?: IBrowserRuntimeOperationOptions,
|
|
2069
|
+
): Promise<IBrowserRuntimeState> {
|
|
2070
|
+
return this.runtime.leaseHistory(this.record, 'reload', options, operationOptions);
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
public setViewport(
|
|
2074
|
+
viewport: plugins.smartpuppeteer.ILiveBrowserViewport,
|
|
2075
|
+
operationOptions?: IBrowserRuntimeOperationOptions,
|
|
2076
|
+
): Promise<void> {
|
|
2077
|
+
return this.runtime.leaseSetViewport(this.record, viewport, operationOptions);
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
public dispatchMouse(
|
|
2081
|
+
input: plugins.smartpuppeteer.ILiveBrowserMouseInput,
|
|
2082
|
+
operationOptions?: IBrowserRuntimeOperationOptions,
|
|
2083
|
+
): Promise<void> {
|
|
2084
|
+
return this.runtime.leaseDispatchRawInput(this.record, 'dispatchMouse', input, operationOptions);
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
public dispatchWheel(
|
|
2088
|
+
input: plugins.smartpuppeteer.ILiveBrowserWheelInput,
|
|
2089
|
+
operationOptions?: IBrowserRuntimeOperationOptions,
|
|
2090
|
+
): Promise<void> {
|
|
2091
|
+
return this.runtime.leaseDispatchRawInput(this.record, 'dispatchWheel', input, operationOptions);
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
public dispatchKey(
|
|
2095
|
+
input: plugins.smartpuppeteer.ILiveBrowserKeyInput,
|
|
2096
|
+
operationOptions?: IBrowserRuntimeOperationOptions,
|
|
2097
|
+
): Promise<void> {
|
|
2098
|
+
return this.runtime.leaseDispatchRawInput(this.record, 'dispatchKey', input, operationOptions);
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
public insertText(
|
|
2102
|
+
input: plugins.smartpuppeteer.ILiveBrowserInsertTextInput,
|
|
2103
|
+
operationOptions?: IBrowserRuntimeOperationOptions,
|
|
2104
|
+
): Promise<void> {
|
|
2105
|
+
return this.runtime.leaseDispatchRawInput(this.record, 'insertText', input, operationOptions);
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
public subscribeEvents(
|
|
2109
|
+
listener: (event: TBrowserRuntimeEvent) => void,
|
|
2110
|
+
): Promise<IBrowserRuntimeFrameSubscription> {
|
|
2111
|
+
return this.runtime.subscribeLeaseEvents(this.record, listener);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
public acknowledgeFrame(
|
|
2115
|
+
acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest,
|
|
2116
|
+
): Promise<boolean> {
|
|
2117
|
+
return this.runtime.acknowledgeLeaseFrame(this.record, acknowledgement);
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
public readArtifact(artifactId: string): Promise<Uint8Array> {
|
|
2121
|
+
return this.runtime.readLeaseArtifact(this.record, artifactId);
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
public deleteArtifact(artifactId: string): Promise<void> {
|
|
2125
|
+
return this.runtime.deleteLeaseArtifact(this.record, artifactId);
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
public cancelActiveOperation(): boolean {
|
|
2129
|
+
return this.runtime.cancelLeaseOperation(this.record);
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
public release(): Promise<void> {
|
|
2133
|
+
return this.runtime.releaseLease(this.record);
|
|
2134
|
+
}
|
|
2135
|
+
}
|