@mearl/provider 2.13.0 → 2.15.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/README.md +104 -83
- package/dist/generated-provider-actions.d.ts +1 -1
- package/dist/generated-provider-actions.js +0 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/provider-runtime.d.ts +5 -0
- package/dist/provider-runtime.js +527 -0
- package/dist/registry.d.ts +25 -11
- package/dist/registry.js +41 -25
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +1 -0
- package/dist/server.js +93 -123
- package/dist/types.d.ts +162 -29
- package/dist/types.js +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import { MEARL_PROVIDER_PROTOCOL_VERSION } from './types.js';
|
|
2
|
+
import { isProviderAction } from './generated-provider-actions.js';
|
|
3
|
+
const LIFECYCLE_OPERATIONS = new Set([
|
|
4
|
+
'start',
|
|
5
|
+
'close',
|
|
6
|
+
'detach',
|
|
7
|
+
'deleteData',
|
|
8
|
+
'access',
|
|
9
|
+
'syncCookies',
|
|
10
|
+
]);
|
|
11
|
+
function isJsonValue(value, depth = 0) {
|
|
12
|
+
if (depth > 32)
|
|
13
|
+
return false;
|
|
14
|
+
if (value === null || ['string', 'boolean'].includes(typeof value))
|
|
15
|
+
return true;
|
|
16
|
+
if (typeof value === 'number')
|
|
17
|
+
return Number.isFinite(value);
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return value.every(item => isJsonValue(item, depth + 1));
|
|
20
|
+
return (typeof value === 'object' &&
|
|
21
|
+
Object.values(value).every(item => isJsonValue(item, depth + 1)));
|
|
22
|
+
}
|
|
23
|
+
function isOptionalString(value) {
|
|
24
|
+
return value === undefined || typeof value === 'string';
|
|
25
|
+
}
|
|
26
|
+
function isOptionalFiniteNumber(value) {
|
|
27
|
+
return value === undefined || (typeof value === 'number' && Number.isFinite(value));
|
|
28
|
+
}
|
|
29
|
+
function isOptionalCount(value) {
|
|
30
|
+
return value === undefined || (Number.isInteger(value) && Number(value) >= 0);
|
|
31
|
+
}
|
|
32
|
+
export function isProviderInputSchema(value, depth = 0) {
|
|
33
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || depth > 32)
|
|
34
|
+
return false;
|
|
35
|
+
const schema = value;
|
|
36
|
+
if (!isOptionalString(schema.title) ||
|
|
37
|
+
!isOptionalString(schema.description) ||
|
|
38
|
+
(schema.component !== undefined &&
|
|
39
|
+
!['cookie-sync', 'switch'].includes(String(schema.component))) ||
|
|
40
|
+
(schema.default !== undefined && !isJsonValue(schema.default)) ||
|
|
41
|
+
(schema.formDefault !== undefined && !isJsonValue(schema.formDefault)) ||
|
|
42
|
+
(schema.formHidden !== undefined && typeof schema.formHidden !== 'boolean') ||
|
|
43
|
+
(schema.formWidth !== undefined && !['half', 'full'].includes(String(schema.formWidth))) ||
|
|
44
|
+
(schema.formHideOptional !== undefined && typeof schema.formHideOptional !== 'boolean') ||
|
|
45
|
+
(schema.formVisibleWhen !== undefined &&
|
|
46
|
+
(!schema.formVisibleWhen ||
|
|
47
|
+
typeof schema.formVisibleWhen !== 'object' ||
|
|
48
|
+
Array.isArray(schema.formVisibleWhen) ||
|
|
49
|
+
typeof schema.formVisibleWhen.field !== 'string' ||
|
|
50
|
+
!schema.formVisibleWhen.field ||
|
|
51
|
+
typeof schema.formVisibleWhen.empty !== 'boolean'))) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (schema.type === 'string') {
|
|
55
|
+
let validPattern = true;
|
|
56
|
+
if (schema.pattern !== undefined) {
|
|
57
|
+
try {
|
|
58
|
+
new RegExp(String(schema.pattern));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
validPattern = false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const valid = (schema.enum === undefined ||
|
|
65
|
+
(Array.isArray(schema.enum) && schema.enum.every(item => typeof item === 'string'))) &&
|
|
66
|
+
(schema.secret === undefined || typeof schema.secret === 'boolean') &&
|
|
67
|
+
isOptionalCount(schema.minLength) &&
|
|
68
|
+
isOptionalCount(schema.maxLength) &&
|
|
69
|
+
(schema.minLength === undefined ||
|
|
70
|
+
schema.maxLength === undefined ||
|
|
71
|
+
Number(schema.minLength) <= Number(schema.maxLength)) &&
|
|
72
|
+
isOptionalString(schema.pattern) &&
|
|
73
|
+
validPattern &&
|
|
74
|
+
isOptionalString(schema.placeholder);
|
|
75
|
+
if (!valid)
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
else if (schema.type === 'number') {
|
|
79
|
+
const valid = (schema.integer === undefined || typeof schema.integer === 'boolean') &&
|
|
80
|
+
isOptionalFiniteNumber(schema.minimum) &&
|
|
81
|
+
isOptionalFiniteNumber(schema.maximum) &&
|
|
82
|
+
(schema.minimum === undefined ||
|
|
83
|
+
schema.maximum === undefined ||
|
|
84
|
+
Number(schema.minimum) <= Number(schema.maximum));
|
|
85
|
+
if (!valid)
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
else if (schema.type === 'boolean') {
|
|
89
|
+
// Boolean schemas have no type-specific constraints.
|
|
90
|
+
}
|
|
91
|
+
else if (schema.type === 'array') {
|
|
92
|
+
if (!isProviderInputSchema(schema.items, depth + 1) ||
|
|
93
|
+
!isOptionalCount(schema.minItems) ||
|
|
94
|
+
!isOptionalCount(schema.maxItems) ||
|
|
95
|
+
(schema.minItems !== undefined &&
|
|
96
|
+
schema.maxItems !== undefined &&
|
|
97
|
+
Number(schema.minItems) > Number(schema.maxItems))) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
if (schema.type !== 'object' ||
|
|
103
|
+
!schema.properties ||
|
|
104
|
+
typeof schema.properties !== 'object' ||
|
|
105
|
+
Array.isArray(schema.properties) ||
|
|
106
|
+
(schema.additionalProperties !== undefined &&
|
|
107
|
+
typeof schema.additionalProperties !== 'boolean')) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const properties = schema.properties;
|
|
111
|
+
if (!Object.values(properties).every(item => isProviderInputSchema(item, depth + 1))) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
if (Object.values(properties).some(item => {
|
|
115
|
+
const condition = item.formVisibleWhen;
|
|
116
|
+
return condition !== undefined && !(String(condition.field) in properties);
|
|
117
|
+
})) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (schema.not !== undefined &&
|
|
121
|
+
(!isProviderInputSchema(schema.not, depth + 1) || schema.not.type !== 'object'))
|
|
122
|
+
return false;
|
|
123
|
+
if (schema.oneOf !== undefined &&
|
|
124
|
+
(!Array.isArray(schema.oneOf) ||
|
|
125
|
+
schema.oneOf.length === 0 ||
|
|
126
|
+
schema.oneOf.some(branch => !isProviderInputSchema(branch, depth + 1) || branch.type !== 'object')))
|
|
127
|
+
return false;
|
|
128
|
+
if (schema.required !== undefined &&
|
|
129
|
+
(!Array.isArray(schema.required) ||
|
|
130
|
+
schema.required.some(item => typeof item !== 'string' || !(item in properties)) ||
|
|
131
|
+
new Set(schema.required).size !== schema.required.length)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (schema.component === 'cookie-sync' && schema.type !== 'object')
|
|
136
|
+
return false;
|
|
137
|
+
if (schema.component === 'switch' && schema.type !== 'boolean')
|
|
138
|
+
return false;
|
|
139
|
+
if (schema.default !== undefined) {
|
|
140
|
+
try {
|
|
141
|
+
validateSchemaValue(value, schema.default, 'default');
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (schema.formDefault !== undefined) {
|
|
148
|
+
try {
|
|
149
|
+
validateSchemaValue(value, schema.formDefault, 'formDefault');
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
function fail(path, message) {
|
|
158
|
+
throw new Error(`${path} ${message}`);
|
|
159
|
+
}
|
|
160
|
+
function validateSchemaValue(schema, value, path) {
|
|
161
|
+
if (schema.type === 'string') {
|
|
162
|
+
if (typeof value !== 'string')
|
|
163
|
+
fail(path, 'must be a string');
|
|
164
|
+
if (schema.enum && !schema.enum.includes(value))
|
|
165
|
+
fail(path, 'has an unsupported value');
|
|
166
|
+
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
167
|
+
fail(path, `must contain at least ${schema.minLength} characters`);
|
|
168
|
+
}
|
|
169
|
+
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
|
|
170
|
+
fail(path, `must contain at most ${schema.maxLength} characters`);
|
|
171
|
+
}
|
|
172
|
+
if (schema.pattern !== undefined && !new RegExp(schema.pattern).test(value)) {
|
|
173
|
+
fail(path, 'has an invalid format');
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (schema.type === 'number') {
|
|
178
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
179
|
+
fail(path, 'must be a number');
|
|
180
|
+
if (schema.integer && !Number.isInteger(value))
|
|
181
|
+
fail(path, 'must be an integer');
|
|
182
|
+
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
183
|
+
fail(path, `must be at least ${schema.minimum}`);
|
|
184
|
+
}
|
|
185
|
+
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
186
|
+
fail(path, `must be at most ${schema.maximum}`);
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (schema.type === 'boolean') {
|
|
191
|
+
if (typeof value !== 'boolean')
|
|
192
|
+
fail(path, 'must be a boolean');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (schema.type === 'array') {
|
|
196
|
+
if (!Array.isArray(value))
|
|
197
|
+
fail(path, 'must be an array');
|
|
198
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
199
|
+
fail(path, `must contain at least ${schema.minItems} items`);
|
|
200
|
+
}
|
|
201
|
+
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
|
|
202
|
+
fail(path, `must contain at most ${schema.maxItems} items`);
|
|
203
|
+
}
|
|
204
|
+
value.forEach((item, index) => validateSchemaValue(schema.items, item, `${path}[${index}]`));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
validateObject(schema, value, path);
|
|
208
|
+
}
|
|
209
|
+
function validateObject(schema, value, path) {
|
|
210
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
211
|
+
fail(path, 'must be an object');
|
|
212
|
+
const input = value;
|
|
213
|
+
for (const key of schema.required ?? []) {
|
|
214
|
+
if (input[key] === undefined)
|
|
215
|
+
fail(`${path}.${key}`, 'is required');
|
|
216
|
+
}
|
|
217
|
+
if (schema.not) {
|
|
218
|
+
let matches = false;
|
|
219
|
+
try {
|
|
220
|
+
validateObject(schema.not, input, path);
|
|
221
|
+
matches = true;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
/* A rejected exclusion is valid. */
|
|
225
|
+
}
|
|
226
|
+
if (matches)
|
|
227
|
+
fail(path, 'matches a forbidden field combination');
|
|
228
|
+
}
|
|
229
|
+
if (schema.oneOf) {
|
|
230
|
+
const matches = schema.oneOf.filter(branch => {
|
|
231
|
+
try {
|
|
232
|
+
validateObject(branch, input, path);
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
if (matches.length !== 1)
|
|
240
|
+
fail(path, 'must match exactly one launch form');
|
|
241
|
+
}
|
|
242
|
+
if (schema.additionalProperties !== true) {
|
|
243
|
+
for (const key of Object.keys(input)) {
|
|
244
|
+
if (!schema.properties[key])
|
|
245
|
+
fail(`${path}.${key}`, 'is not supported');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (const [key, item] of Object.entries(input)) {
|
|
249
|
+
const property = schema.properties[key];
|
|
250
|
+
if (property)
|
|
251
|
+
validateSchemaValue(property, item, `${path}.${key}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
export function validateProviderInput(schema, value) {
|
|
255
|
+
validateObject(schema, value, 'options');
|
|
256
|
+
}
|
|
257
|
+
export function validateProviderBrowser(browser) {
|
|
258
|
+
if (!browser || typeof browser !== 'object')
|
|
259
|
+
throw new Error('Provider returned no browser');
|
|
260
|
+
if (typeof browser.browserId !== 'string' || !browser.browserId.trim()) {
|
|
261
|
+
throw new Error('Provider browserId is required');
|
|
262
|
+
}
|
|
263
|
+
if (typeof browser.name !== 'string' || !browser.name.trim()) {
|
|
264
|
+
throw new Error(`Provider browser ${browser.browserId} needs a name`);
|
|
265
|
+
}
|
|
266
|
+
if (!['connected', 'pairing', 'running_disconnected', 'stopped'].includes(browser.status)) {
|
|
267
|
+
throw new Error(`Provider browser ${browser.browserId} has an invalid status`);
|
|
268
|
+
}
|
|
269
|
+
if (!['ephemeral', 'persistent'].includes(browser.persistence)) {
|
|
270
|
+
throw new Error(`Provider browser ${browser.browserId} has invalid persistence`);
|
|
271
|
+
}
|
|
272
|
+
if (browser.ownership !== undefined && !['owned', 'attached'].includes(browser.ownership)) {
|
|
273
|
+
throw new Error(`Provider browser ${browser.browserId} has invalid ownership`);
|
|
274
|
+
}
|
|
275
|
+
if (!browser.control || !['provider', 'host', 'none'].includes(browser.control.kind)) {
|
|
276
|
+
throw new Error(`Provider browser ${browser.browserId} has invalid control`);
|
|
277
|
+
}
|
|
278
|
+
if (!Array.isArray(browser.operations) ||
|
|
279
|
+
new Set(browser.operations).size !== browser.operations.length ||
|
|
280
|
+
browser.operations.some(operation => !LIFECYCLE_OPERATIONS.has(operation))) {
|
|
281
|
+
throw new Error(`Provider browser ${browser.browserId} has invalid lifecycle operations`);
|
|
282
|
+
}
|
|
283
|
+
if (browser.status === 'pairing') {
|
|
284
|
+
if (browser.pairing?.kind !== 'qr' ||
|
|
285
|
+
typeof browser.pairing.url !== 'string' ||
|
|
286
|
+
!browser.pairing.url.trim()) {
|
|
287
|
+
throw new Error(`Pairing browser ${browser.browserId} needs a pairing URL`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else if (browser.pairing) {
|
|
291
|
+
throw new Error(`Pairing metadata requires a pairing state: ${browser.browserId}`);
|
|
292
|
+
}
|
|
293
|
+
if (browser.pairing &&
|
|
294
|
+
(!isOptionalString(browser.pairing.code) || !isOptionalString(browser.pairing.label))) {
|
|
295
|
+
throw new Error(`Provider browser ${browser.browserId} has invalid pairing metadata`);
|
|
296
|
+
}
|
|
297
|
+
if (browser.lastFocusedAt !== undefined &&
|
|
298
|
+
(!Number.isFinite(browser.lastFocusedAt) || browser.lastFocusedAt < 0)) {
|
|
299
|
+
throw new Error(`Provider browser ${browser.browserId} has an invalid lastFocusedAt`);
|
|
300
|
+
}
|
|
301
|
+
if (browser.status === 'stopped' && browser.control.kind !== 'none') {
|
|
302
|
+
throw new Error(`Stopped Provider browser ${browser.browserId} cannot expose live control`);
|
|
303
|
+
}
|
|
304
|
+
if (browser.status === 'connected' && browser.control.kind === 'none') {
|
|
305
|
+
throw new Error(`Connected Provider browser ${browser.browserId} must expose control`);
|
|
306
|
+
}
|
|
307
|
+
const capabilities = browser.control.kind === 'provider' || browser.control.kind === 'host'
|
|
308
|
+
? browser.control.capabilities
|
|
309
|
+
: undefined;
|
|
310
|
+
if (browser.control.kind === 'provider' || capabilities) {
|
|
311
|
+
if (!Array.isArray(capabilities?.actions)) {
|
|
312
|
+
throw new Error(`Provider browser ${browser.browserId} needs action capabilities`);
|
|
313
|
+
}
|
|
314
|
+
if (capabilities.actions.some(action => !isProviderAction(action)) ||
|
|
315
|
+
new Set(capabilities.actions).size !== capabilities.actions.length) {
|
|
316
|
+
throw new Error(`Provider browser ${browser.browserId} has invalid action capabilities`);
|
|
317
|
+
}
|
|
318
|
+
if (!['native', 'reconstructed', 'none'].includes(capabilities.screenshot)) {
|
|
319
|
+
throw new Error(`Provider browser ${browser.browserId} has invalid screenshot capabilities`);
|
|
320
|
+
}
|
|
321
|
+
const supportsScreenshot = capabilities.actions.includes('page_screenshot');
|
|
322
|
+
if (supportsScreenshot === (capabilities.screenshot === 'none')) {
|
|
323
|
+
throw new Error(`Provider browser ${browser.browserId} has inconsistent screenshot capabilities`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (browser.control.kind === 'host' &&
|
|
327
|
+
(!['extension', 'cdp'].includes(browser.control.transport) ||
|
|
328
|
+
typeof browser.control.browserId !== 'string' ||
|
|
329
|
+
!browser.control.browserId.trim())) {
|
|
330
|
+
throw new Error(`Provider browser ${browser.browserId} has invalid host control`);
|
|
331
|
+
}
|
|
332
|
+
if (browser.references !== undefined &&
|
|
333
|
+
(!Array.isArray(browser.references) ||
|
|
334
|
+
browser.references.some(reference => !reference ||
|
|
335
|
+
typeof reference.kind !== 'string' ||
|
|
336
|
+
!reference.kind.trim() ||
|
|
337
|
+
typeof reference.id !== 'string' ||
|
|
338
|
+
!reference.id.trim() ||
|
|
339
|
+
!isOptionalString(reference.label)) ||
|
|
340
|
+
new Set(browser.references.map(reference => `${reference.kind}:${reference.id}`)).size !==
|
|
341
|
+
browser.references.length)) {
|
|
342
|
+
throw new Error(`Provider browser ${browser.browserId} has invalid references`);
|
|
343
|
+
}
|
|
344
|
+
if (browser.details !== undefined &&
|
|
345
|
+
(!browser.details ||
|
|
346
|
+
typeof browser.details !== 'object' ||
|
|
347
|
+
Array.isArray(browser.details) ||
|
|
348
|
+
!isJsonValue(browser.details))) {
|
|
349
|
+
throw new Error(`Provider browser ${browser.browserId} has invalid details`);
|
|
350
|
+
}
|
|
351
|
+
if (browser.details) {
|
|
352
|
+
const details = browser.details;
|
|
353
|
+
const strings = (value) => Array.isArray(value) && value.every(item => typeof item === 'string');
|
|
354
|
+
if ((details.headless !== undefined && typeof details.headless !== 'boolean') ||
|
|
355
|
+
(details.createdAt !== undefined &&
|
|
356
|
+
(typeof details.createdAt !== 'string' ||
|
|
357
|
+
!Number.isFinite(Date.parse(details.createdAt)))) ||
|
|
358
|
+
(details.cdpPort !== undefined &&
|
|
359
|
+
(!Number.isInteger(details.cdpPort) || details.cdpPort < 1 || details.cdpPort > 65535)) ||
|
|
360
|
+
(details.copiedCookies !== undefined &&
|
|
361
|
+
(!details.copiedCookies ||
|
|
362
|
+
!strings(details.copiedCookies.domains) ||
|
|
363
|
+
!Number.isInteger(details.copiedCookies.count) ||
|
|
364
|
+
details.copiedCookies.count < 0))) {
|
|
365
|
+
throw new Error(`Provider browser ${browser.browserId} has invalid common details`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function validateDefinition(definition) {
|
|
370
|
+
const { providerId, name, version, protocolVersion } = definition.descriptor;
|
|
371
|
+
if (!providerId?.trim())
|
|
372
|
+
throw new Error('providerId is required');
|
|
373
|
+
if (!name?.trim())
|
|
374
|
+
throw new Error('provider name is required');
|
|
375
|
+
if (!version?.trim())
|
|
376
|
+
throw new Error('provider version is required');
|
|
377
|
+
if (protocolVersion !== undefined && protocolVersion !== MEARL_PROVIDER_PROTOCOL_VERSION) {
|
|
378
|
+
throw new Error(`Unsupported provider protocol ${protocolVersion}; expected ${MEARL_PROVIDER_PROTOCOL_VERSION}`);
|
|
379
|
+
}
|
|
380
|
+
if (!definition.browserType.name?.trim())
|
|
381
|
+
throw new Error('provider browser type name is required');
|
|
382
|
+
if (!isProviderInputSchema(definition.browserType.inputSchema) ||
|
|
383
|
+
definition.browserType.inputSchema.type !== 'object') {
|
|
384
|
+
throw new Error('Provider input schema is invalid');
|
|
385
|
+
}
|
|
386
|
+
if (definition.browserType.documentationUrl !== undefined &&
|
|
387
|
+
(typeof definition.browserType.documentationUrl !== 'string' ||
|
|
388
|
+
!/^https?:\/\//.test(definition.browserType.documentationUrl)))
|
|
389
|
+
throw new Error('Provider documentation URL must use HTTP(S)');
|
|
390
|
+
const configuration = definition.browserType.configuration;
|
|
391
|
+
if (configuration &&
|
|
392
|
+
(!isProviderInputSchema(configuration.inputSchema) ||
|
|
393
|
+
configuration.inputSchema.type !== 'object')) {
|
|
394
|
+
throw new Error('Provider configuration schema is invalid');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
export function createProviderRuntime(definition) {
|
|
398
|
+
validateDefinition(definition);
|
|
399
|
+
let publishedBrowsers = [];
|
|
400
|
+
const validateRuntimeBrowser = (browser) => {
|
|
401
|
+
validateProviderBrowser(browser);
|
|
402
|
+
const implementations = {
|
|
403
|
+
start: definition.start,
|
|
404
|
+
detach: definition.detach,
|
|
405
|
+
access: definition.getAccess,
|
|
406
|
+
syncCookies: definition.syncCookies,
|
|
407
|
+
};
|
|
408
|
+
for (const [operation, implementation] of Object.entries(implementations)) {
|
|
409
|
+
if (browser.operations.includes(operation) &&
|
|
410
|
+
!implementation) {
|
|
411
|
+
throw new Error(`Provider browser ${browser.browserId} advertises ${operation} without implementing it`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
const listBrowsers = async (context = {}) => {
|
|
416
|
+
const browsers = definition.listBrowsers
|
|
417
|
+
? await definition.listBrowsers(context)
|
|
418
|
+
: publishedBrowsers;
|
|
419
|
+
const seen = new Set();
|
|
420
|
+
for (const browser of browsers) {
|
|
421
|
+
validateRuntimeBrowser(browser);
|
|
422
|
+
if (seen.has(browser.browserId)) {
|
|
423
|
+
throw new Error(`Duplicate provider browserId: ${browser.browserId}`);
|
|
424
|
+
}
|
|
425
|
+
seen.add(browser.browserId);
|
|
426
|
+
}
|
|
427
|
+
return browsers;
|
|
428
|
+
};
|
|
429
|
+
const requireBrowser = async (browserId, operation, context) => {
|
|
430
|
+
const browser = (await listBrowsers(context)).find(item => item.browserId === browserId);
|
|
431
|
+
if (!browser)
|
|
432
|
+
throw new Error(`Provider browser not found: ${browserId}`);
|
|
433
|
+
if (!browser.operations.includes(operation)) {
|
|
434
|
+
throw new Error(`Provider browser ${browserId} does not support ${operation}`);
|
|
435
|
+
}
|
|
436
|
+
return browser;
|
|
437
|
+
};
|
|
438
|
+
return {
|
|
439
|
+
definition,
|
|
440
|
+
listBrowsers,
|
|
441
|
+
updateBrowsers(browsers) {
|
|
442
|
+
browsers.forEach(validateRuntimeBrowser);
|
|
443
|
+
if (new Set(browsers.map(browser => browser.browserId)).size !== browsers.length) {
|
|
444
|
+
throw new Error('Provider browser ids must be unique');
|
|
445
|
+
}
|
|
446
|
+
publishedBrowsers = [...browsers];
|
|
447
|
+
},
|
|
448
|
+
async launch(options, context = {}) {
|
|
449
|
+
validateProviderInput(definition.browserType.inputSchema, options);
|
|
450
|
+
const browser = await definition.launch(options, context);
|
|
451
|
+
validateRuntimeBrowser(browser);
|
|
452
|
+
return browser;
|
|
453
|
+
},
|
|
454
|
+
async start(request, context = {}) {
|
|
455
|
+
await requireBrowser(request.browserId, 'start', context);
|
|
456
|
+
if (!definition.start)
|
|
457
|
+
throw new Error('Provider does not implement start');
|
|
458
|
+
const browser = await definition.start(request, context);
|
|
459
|
+
validateRuntimeBrowser(browser);
|
|
460
|
+
return browser;
|
|
461
|
+
},
|
|
462
|
+
async close(request, context = {}) {
|
|
463
|
+
await requireBrowser(request.browserId, request.deleteData ? 'deleteData' : 'close', context);
|
|
464
|
+
return definition.close(request, context);
|
|
465
|
+
},
|
|
466
|
+
async detach(request, context = {}) {
|
|
467
|
+
await requireBrowser(request.browserId, 'detach', context);
|
|
468
|
+
if (!definition.detach)
|
|
469
|
+
throw new Error('Provider does not implement detach');
|
|
470
|
+
return definition.detach(request, context);
|
|
471
|
+
},
|
|
472
|
+
async syncCookies(request, context = {}) {
|
|
473
|
+
await requireBrowser(request.browserId, 'syncCookies', context);
|
|
474
|
+
if (!definition.syncCookies)
|
|
475
|
+
throw new Error('Provider does not implement cookie sync');
|
|
476
|
+
if (!request.scope ||
|
|
477
|
+
typeof request.scope !== 'object' ||
|
|
478
|
+
Array.isArray(request.scope) ||
|
|
479
|
+
!isJsonValue(request.scope)) {
|
|
480
|
+
throw new Error('Cookie sync scope must be a JSON object');
|
|
481
|
+
}
|
|
482
|
+
return definition.syncCookies(request, context);
|
|
483
|
+
},
|
|
484
|
+
async getAccess(request, context = {}) {
|
|
485
|
+
await requireBrowser(request.browserId, 'access', context);
|
|
486
|
+
if (!definition.getAccess)
|
|
487
|
+
throw new Error('Provider does not expose browser access');
|
|
488
|
+
const access = await definition.getAccess(request, context);
|
|
489
|
+
if (!access ||
|
|
490
|
+
typeof access.kind !== 'string' ||
|
|
491
|
+
!access.kind.trim() ||
|
|
492
|
+
typeof access.url !== 'string') {
|
|
493
|
+
throw new Error('Provider returned an invalid browser access result');
|
|
494
|
+
}
|
|
495
|
+
const url = new URL(access.url);
|
|
496
|
+
if (!['http:', 'https:'].includes(url.protocol) || access.sensitive !== true) {
|
|
497
|
+
throw new Error('Provider returned an invalid browser access result');
|
|
498
|
+
}
|
|
499
|
+
return { ...access, url: url.toString() };
|
|
500
|
+
},
|
|
501
|
+
async configure(options, context = {}) {
|
|
502
|
+
const configuration = definition.browserType.configuration;
|
|
503
|
+
if (!configuration || !definition.configure) {
|
|
504
|
+
throw new Error('Provider does not support configuration');
|
|
505
|
+
}
|
|
506
|
+
validateProviderInput(configuration.inputSchema, options);
|
|
507
|
+
return definition.configure(options, context);
|
|
508
|
+
},
|
|
509
|
+
async invokeAction(actionContext) {
|
|
510
|
+
const browser = (await listBrowsers({ controlSource: actionContext.controlSource })).find(item => item.browserId === actionContext.browserId);
|
|
511
|
+
if (!browser)
|
|
512
|
+
throw new Error(`Provider browser not found: ${actionContext.browserId}`);
|
|
513
|
+
if (browser.status !== 'connected') {
|
|
514
|
+
throw new Error(`Provider browser ${actionContext.browserId} is not connected`);
|
|
515
|
+
}
|
|
516
|
+
if (browser.control.kind !== 'provider') {
|
|
517
|
+
throw new Error(`Provider browser ${actionContext.browserId} uses host control`);
|
|
518
|
+
}
|
|
519
|
+
if (!browser.control.capabilities.actions.includes(actionContext.action)) {
|
|
520
|
+
throw new Error(`Provider browser ${actionContext.browserId} does not support ${actionContext.action}`);
|
|
521
|
+
}
|
|
522
|
+
if (!definition.handleAction)
|
|
523
|
+
throw new Error('Provider does not implement browser actions');
|
|
524
|
+
return definition.handleAction(actionContext);
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -27,12 +27,22 @@ export interface ProviderBrowserRecord extends DaemonRecord {
|
|
|
27
27
|
browserName: string;
|
|
28
28
|
socketPath: string;
|
|
29
29
|
transport: 'provider';
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
provider: {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
builtin: boolean;
|
|
34
|
+
version: string;
|
|
35
|
+
protocolVersion: number;
|
|
36
|
+
};
|
|
37
|
+
capabilities?: Extract<ProviderBrowser['control'], {
|
|
38
|
+
kind: 'provider';
|
|
39
|
+
}>['capabilities'];
|
|
40
|
+
status: ProviderBrowser['status'];
|
|
41
|
+
ownership?: ProviderBrowser['ownership'];
|
|
42
|
+
persistence: ProviderBrowser['persistence'];
|
|
43
|
+
operations: ProviderBrowser['operations'];
|
|
44
|
+
references?: ProviderBrowser['references'];
|
|
45
|
+
details?: ProviderBrowser['details'];
|
|
36
46
|
pairing?: ProviderBrowser['pairing'];
|
|
37
47
|
startedAt: string;
|
|
38
48
|
updatedAt: number;
|
|
@@ -45,12 +55,16 @@ export interface RegisteredBrowserRecord extends DaemonRecord {
|
|
|
45
55
|
transport: 'extension' | 'cdp' | 'provider';
|
|
46
56
|
extensionVersion?: string;
|
|
47
57
|
nativeHostVersion?: string;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
capabilities?: ProviderBrowser['capabilities'];
|
|
58
|
+
provider?: ProviderBrowserRecord['provider'];
|
|
59
|
+
capabilities?: Extract<ProviderBrowser['control'], {
|
|
60
|
+
kind: 'provider';
|
|
61
|
+
}>['capabilities'];
|
|
53
62
|
status?: ProviderBrowser['status'];
|
|
63
|
+
ownership?: ProviderBrowserRecord['ownership'];
|
|
64
|
+
persistence?: ProviderBrowserRecord['persistence'];
|
|
65
|
+
operations?: ProviderBrowserRecord['operations'];
|
|
66
|
+
references?: ProviderBrowser['references'];
|
|
67
|
+
details?: ProviderBrowser['details'];
|
|
54
68
|
pairing?: ProviderBrowser['pairing'];
|
|
55
69
|
startedAt?: string;
|
|
56
70
|
updatedAt?: number;
|