@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/dist/registry.js CHANGED
@@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto';
3
3
  import { isAbsolute, join, relative, resolve, sep } from 'node:path';
4
4
  import { claimRecord, ensureDir, hashKey, isAlive, keyedConfigFile, keyedDir, listRecordFiles, readLiveRecords, readRecord, removeInvalidRecord, removeRecordIfPid, writeRecord, writeRecordIfPid, } from '@mearl/daemon-core';
5
5
  import { MEARL_PROVIDER_PROTOCOL_VERSION, } from './types.js';
6
+ import { isProviderInputSchema } from './provider-runtime.js';
6
7
  export const INSTALLED_PROVIDERS_SUBDIR = 'providers/installed';
7
8
  export const LIVE_PROVIDERS_SUBDIR = 'providers/live';
8
9
  export const STARTING_PROVIDERS_SUBDIR = 'providers/starting';
@@ -10,8 +11,14 @@ export const PROVIDER_SOCKETS_SUBDIR = 'providers/sockets';
10
11
  export const PROVIDER_DATA_SUBDIR = 'providers/data';
11
12
  export const BROWSERS_SUBDIR = 'browsers';
12
13
  export const PROVIDER_START_TOKEN_ENV = 'MEARL_PROVIDER_START_TOKEN';
14
+ const RESERVED_PROVIDER_IDS = new Set(['local']);
13
15
  export function providerBrowserKey(providerId, browserId) {
14
- return `provider:${encodeURIComponent(providerId)}:${encodeURIComponent(browserId)}`;
16
+ const redundantPrefix = `managed:${providerId}:`;
17
+ const scopedBrowserId = browserId.toLowerCase().startsWith(redundantPrefix.toLowerCase())
18
+ ? browserId.slice(redundantPrefix.length)
19
+ : browserId;
20
+ const encodedBrowserId = encodeURIComponent(scopedBrowserId).replace(/%3A/gi, ':');
21
+ return `provider:${encodeURIComponent(providerId)}:${encodedBrowserId}`;
15
22
  }
16
23
  export function providerSocketPath(providerId) {
17
24
  const suffix = hashKey(providerId);
@@ -89,6 +96,9 @@ export function validateInstalledProviderManifest(manifest) {
89
96
  if (manifest.protocolVersion !== MEARL_PROVIDER_PROTOCOL_VERSION) {
90
97
  return `protocol ${manifest.protocolVersion} is unsupported`;
91
98
  }
99
+ if (RESERVED_PROVIDER_IDS.has(manifest.providerId)) {
100
+ return `providerId ${manifest.providerId} is reserved for a built-in Provider`;
101
+ }
92
102
  if (!isProviderBrowserType(manifest.browserType)) {
93
103
  return 'browserType is invalid';
94
104
  }
@@ -135,25 +145,23 @@ function isProviderBrowserType(value) {
135
145
  const browserType = value;
136
146
  if (typeof browserType.name !== 'string' || !browserType.name.trim())
137
147
  return false;
138
- if (!Array.isArray(browserType.parameters))
148
+ if (browserType.documentationUrl !== undefined &&
149
+ (typeof browserType.documentationUrl !== 'string' ||
150
+ !/^https?:\/\//.test(browserType.documentationUrl)))
139
151
  return false;
140
- const keys = new Set();
141
- for (const parameter of browserType.parameters) {
142
- if (!parameter ||
143
- typeof parameter !== 'object' ||
144
- typeof parameter.key !== 'string' ||
145
- !/^[A-Za-z][A-Za-z0-9_]*$/.test(parameter.key) ||
146
- typeof parameter.label !== 'string' ||
147
- !parameter.label.trim() ||
148
- (parameter.required !== undefined && typeof parameter.required !== 'boolean') ||
149
- (parameter.placeholder !== undefined && typeof parameter.placeholder !== 'string') ||
150
- (parameter.description !== undefined && typeof parameter.description !== 'string') ||
151
- keys.has(parameter.key)) {
152
- return false;
153
- }
154
- keys.add(parameter.key);
155
- }
156
- return true;
152
+ if (browserType.help !== undefined &&
153
+ (!Array.isArray(browserType.help) ||
154
+ browserType.help.some(item => typeof item !== 'string' || !item.trim())))
155
+ return false;
156
+ if (!isProviderObjectSchema(browserType.inputSchema))
157
+ return false;
158
+ return (browserType.configuration === undefined ||
159
+ (Boolean(browserType.configuration) &&
160
+ typeof browserType.configuration === 'object' &&
161
+ isProviderObjectSchema(browserType.configuration.inputSchema)));
162
+ }
163
+ function isProviderObjectSchema(value) {
164
+ return isProviderInputSchema(value) && value.type === 'object';
157
165
  }
158
166
  export function registerProviderPackage(packageRoot) {
159
167
  if (!isAbsolute(packageRoot))
@@ -290,13 +298,21 @@ export function publishProviderBrowsers(descriptor, socketPath, browsers, previo
290
298
  browserName: browser.name,
291
299
  socketPath,
292
300
  transport: 'provider',
293
- providerId: descriptor.providerId,
294
- providerName: descriptor.name,
295
- providerVersion: descriptor.version,
296
- providerProtocolVersion: descriptor.protocolVersion ?? MEARL_PROVIDER_PROTOCOL_VERSION,
297
- capabilities: browser.capabilities,
298
- status: browser.status ?? 'connected',
301
+ provider: {
302
+ id: descriptor.providerId,
303
+ name: descriptor.name,
304
+ builtin: descriptor.builtin === true,
305
+ version: descriptor.version,
306
+ protocolVersion: descriptor.protocolVersion ?? MEARL_PROVIDER_PROTOCOL_VERSION,
307
+ },
308
+ capabilities: browser.control.kind === 'provider' ? browser.control.capabilities : undefined,
309
+ status: browser.status,
310
+ ownership: browser.ownership,
311
+ persistence: browser.persistence,
312
+ operations: browser.operations,
299
313
  pairing: browser.pairing,
314
+ references: browser.references,
315
+ details: browser.details,
300
316
  startedAt: previous?.startedAt ?? startedAt,
301
317
  updatedAt: now,
302
318
  lastFocusedAt: Math.max(browser.lastFocusedAt ?? 0, previous?.lastFocusedAt ?? 0),
package/dist/runtime.d.ts CHANGED
@@ -2,3 +2,4 @@
2
2
  export { ensureInstalledProvidersRunning } from './launcher.js';
3
3
  export { providerBrowserKey, readLiveProvider } from './registry.js';
4
4
  export type { LiveProviderRecord } from './registry.js';
5
+ export { createProviderRuntime, validateProviderBrowser, validateProviderInput, } from './provider-runtime.js';
package/dist/runtime.js CHANGED
@@ -1,3 +1,4 @@
1
1
  /** Mearl host integration; Provider implementations should use the package root API. */
2
2
  export { ensureInstalledProvidersRunning } from './launcher.js';
3
3
  export { providerBrowserKey, readLiveProvider } from './registry.js';
4
+ export { createProviderRuntime, validateProviderBrowser, validateProviderInput, } from './provider-runtime.js';
package/dist/server.js CHANGED
@@ -4,6 +4,7 @@ import { chmodSync, rmSync } from 'node:fs';
4
4
  import { dirname } from 'node:path';
5
5
  import { ensureDir } from '@mearl/daemon-core';
6
6
  import { isProviderAction } from './generated-provider-actions.js';
7
+ import { createProviderRuntime } from './provider-runtime.js';
7
8
  import { PROVIDER_START_TOKEN_ENV, claimProviderStart, providerBrowserKey, providerSocketPath, publishLiveProvider, publishProviderBrowsers, readLiveProvider, releaseProviderStart, removeStaleProviderSocket, touchProviderBrowser, unpublishLiveProvider, unpublishProviderBrowsers, } from './registry.js';
8
9
  import { MEARL_PROVIDER_PROTOCOL_VERSION } from './types.js';
9
10
  const MAX_REQUEST_BYTES = 16 * 1024 * 1024;
@@ -11,6 +12,15 @@ const activeProviderIds = new Set();
11
12
  function errorMessage(error) {
12
13
  return error instanceof Error ? error.message : String(error);
13
14
  }
15
+ function isProviderHostContextData(value) {
16
+ if (!value || typeof value !== 'object' || Array.isArray(value))
17
+ return false;
18
+ const host = value;
19
+ return ((host.userAgent === undefined || typeof host.userAgent === 'string') &&
20
+ (host.cookies === undefined ||
21
+ (Array.isArray(host.cookies) &&
22
+ host.cookies.every(cookie => cookie && typeof cookie === 'object' && !Array.isArray(cookie)))));
23
+ }
14
24
  function isProviderActionRequest(value) {
15
25
  if (!value || typeof value !== 'object' || Array.isArray(value))
16
26
  return false;
@@ -25,156 +35,116 @@ function isProviderActionRequest(value) {
25
35
  !Array.isArray(request.data))) &&
26
36
  (request.browserId === undefined || typeof request.browserId === 'string') &&
27
37
  (request.version === undefined || typeof request.version === 'string') &&
28
- (request.controlSource === undefined || typeof request.controlSource === 'string'));
38
+ (request.controlSource === undefined || typeof request.controlSource === 'string') &&
39
+ (request.host === undefined || isProviderHostContextData(request.host)));
40
+ }
41
+ function providerContext(request) {
42
+ const hostData = request.host;
43
+ return {
44
+ controlSource: request.controlSource,
45
+ ...(hostData
46
+ ? {
47
+ host: {
48
+ async request(method) {
49
+ if (method === 'export_cookies')
50
+ return (hostData.cookies ?? []);
51
+ if (method === 'get_browser_user_agent') {
52
+ if (!hostData.userAgent)
53
+ throw new Error('Host browser user agent is unavailable');
54
+ return hostData.userAgent;
55
+ }
56
+ throw new Error(`Unsupported Provider host service: ${method}`);
57
+ },
58
+ },
59
+ }
60
+ : {}),
61
+ };
29
62
  }
30
63
  export function createProviderServer(options) {
31
- const socketPath = options.socketPath ?? providerSocketPath(options.descriptor.providerId);
64
+ const runtime = createProviderRuntime(options.definition);
65
+ const descriptor = options.definition.descriptor;
66
+ const socketPath = options.socketPath ?? providerSocketPath(descriptor.providerId);
32
67
  let server = null;
33
68
  let browserIds = new Set();
34
69
  let browsers = [];
35
70
  let state = 'idle';
36
71
  let stopPromise = null;
37
72
  const connections = new Set();
38
- function assertDescriptor() {
39
- const { providerId, name, version, protocolVersion } = options.descriptor;
40
- if (typeof providerId !== 'string' || !providerId.trim()) {
41
- throw new Error('providerId is required');
42
- }
43
- if (typeof name !== 'string' || !name.trim())
44
- throw new Error('provider name is required');
45
- if (typeof version !== 'string' || !version.trim()) {
46
- throw new Error('provider version is required');
47
- }
48
- if (protocolVersion !== undefined && protocolVersion !== MEARL_PROVIDER_PROTOCOL_VERSION) {
49
- throw new Error(`Unsupported provider protocol ${protocolVersion}; expected ${MEARL_PROVIDER_PROTOCOL_VERSION}`);
50
- }
51
- }
52
- function assertBrowsers(nextBrowsers) {
53
- const seen = new Set();
54
- for (const browser of nextBrowsers) {
55
- if (typeof browser.browserId !== 'string' || !browser.browserId.trim()) {
56
- throw new Error('provider browserId is required');
57
- }
58
- if (typeof browser.name !== 'string' || !browser.name.trim()) {
59
- throw new Error(`Provider browser ${browser.browserId} needs a name`);
60
- }
61
- if (seen.has(browser.browserId)) {
62
- throw new Error(`Duplicate provider browserId: ${browser.browserId}`);
63
- }
64
- seen.add(browser.browserId);
65
- if (!Array.isArray(browser.capabilities?.actions)) {
66
- throw new Error(`Provider browser ${browser.browserId} needs an actions capability list`);
67
- }
68
- for (const action of browser.capabilities.actions) {
69
- if (!isProviderAction(action)) {
70
- throw new Error(`Unsupported provider capability action: ${String(action)}`);
71
- }
72
- }
73
- if (new Set(browser.capabilities.actions).size !== browser.capabilities.actions.length) {
74
- throw new Error(`Provider browser ${browser.browserId} has duplicate capability actions`);
75
- }
76
- if (!['native', 'reconstructed', 'none'].includes(browser.capabilities.screenshot)) {
77
- throw new Error(`Unsupported screenshot capability: ${browser.capabilities.screenshot}`);
78
- }
79
- const supportsScreenshot = browser.capabilities.actions.includes('page_screenshot');
80
- if (supportsScreenshot === (browser.capabilities.screenshot === 'none')) {
81
- throw new Error(`Provider browser ${browser.browserId} has inconsistent screenshot capabilities`);
82
- }
83
- if (browser.status !== undefined && !['connected', 'pairing'].includes(browser.status)) {
84
- throw new Error(`Unsupported provider browser status: ${String(browser.status)}`);
85
- }
86
- assertPairing(browser.status, browser.pairing, browser.browserId);
87
- if (browser.lastFocusedAt !== undefined &&
88
- (!Number.isFinite(browser.lastFocusedAt) || browser.lastFocusedAt < 0)) {
89
- throw new Error(`Provider browser ${browser.browserId} has invalid lastFocusedAt`);
90
- }
91
- }
92
- }
93
- function providerBrowserId(requested) {
73
+ async function providerBrowserId(requested) {
94
74
  if (requested)
95
75
  return requested;
96
- if (browsers.length === 1)
97
- return browsers[0].browserId;
98
- if (browsers.length === 0)
99
- throw new Error('Provider has no connected browsers');
76
+ const available = await runtime.listBrowsers();
77
+ if (available.length === 1)
78
+ return available[0].browserId;
79
+ if (available.length === 0)
80
+ throw new Error('Provider has no browser instances');
100
81
  throw new Error('browserId is required when the provider has multiple browsers');
101
82
  }
102
83
  async function dispatch(request) {
84
+ const context = providerContext(request);
103
85
  if (request.action === 'browser_launch') {
104
86
  if (request.browserId)
105
87
  throw new Error('browser_launch does not accept browserId');
106
- const parameters = request.data ?? {};
107
- if (Object.values(parameters).some(value => typeof value !== 'string')) {
108
- throw new Error('Provider browser parameters must be strings');
109
- }
110
- const created = await options.createBrowser(parameters);
111
- if (!created ||
112
- typeof created.browserId !== 'string' ||
113
- !created.browserId.trim() ||
114
- typeof created.name !== 'string' ||
115
- !created.name.trim() ||
116
- !['connected', 'pairing'].includes(created.status)) {
117
- throw new Error('Provider returned an invalid browser creation result');
118
- }
119
- try {
120
- assertPairing(created.status, created.pairing, created.browserId);
121
- }
122
- catch {
123
- throw new Error('Provider returned an invalid browser creation result');
124
- }
125
- return created;
88
+ return runtime.launch(request.data ?? {}, context);
89
+ }
90
+ if (request.action === 'browser_provider_configure') {
91
+ if (request.browserId)
92
+ throw new Error('Provider configuration does not accept browserId');
93
+ return runtime.configure(request.data ?? {}, context);
94
+ }
95
+ if (request.action === 'browser_manager_start') {
96
+ return runtime.start({ browserId: await providerBrowserId(request.browserId) }, context);
97
+ }
98
+ if (request.action === 'browser_manager_sync_cookies') {
99
+ return runtime.syncCookies({
100
+ browserId: await providerBrowserId(request.browserId),
101
+ scope: request.data?.scope,
102
+ }, context);
126
103
  }
127
104
  if (request.action !== 'get_versions' &&
128
105
  request.action !== 'browser_close' &&
106
+ request.action !== 'get_browser_access_url' &&
129
107
  !isProviderAction(request.action)) {
130
108
  throw new Error(`Unsupported provider action: ${String(request.action)}`);
131
109
  }
132
- const browserId = providerBrowserId(request.browserId);
133
- const found = browsers.find(browser => browser.browserId === browserId);
134
- if (!found)
135
- throw new Error(`Provider browser not found: ${browserId}`);
110
+ const browserId = await providerBrowserId(request.browserId);
136
111
  if (request.action === 'browser_close') {
137
- return options.deleteBrowser(browserId);
112
+ return runtime.close({ browserId, deleteData: request.data?.deleteProfile === true }, context);
113
+ }
114
+ if (request.action === 'get_browser_access_url') {
115
+ return {
116
+ browserAccess: await runtime.getAccess({ browserId }, context),
117
+ };
138
118
  }
139
119
  if (request.action === 'get_versions') {
120
+ const found = (await runtime.listBrowsers()).find(browser => browser.browserId === browserId);
121
+ if (!found)
122
+ throw new Error(`Provider browser not found: ${browserId}`);
140
123
  return {
141
124
  provider: {
142
- providerId: options.descriptor.providerId,
143
- name: options.descriptor.name,
144
- version: options.descriptor.version,
145
- protocolVersion: options.descriptor.protocolVersion ?? MEARL_PROVIDER_PROTOCOL_VERSION,
125
+ id: descriptor.providerId,
126
+ name: descriptor.name,
127
+ builtin: descriptor.builtin === true,
128
+ version: descriptor.version,
129
+ protocolVersion: descriptor.protocolVersion ?? MEARL_PROVIDER_PROTOCOL_VERSION,
146
130
  },
147
- capabilities: found.capabilities,
131
+ capabilities: found.control.kind === 'provider' ? found.control.capabilities : undefined,
148
132
  };
149
133
  }
150
- if (found.status === 'pairing') {
151
- throw new Error(`Provider browser ${browserId} is waiting for device pairing`);
152
- }
153
- if (!found.capabilities.actions.includes(request.action)) {
154
- throw new Error(`Provider browser ${browserId} does not support ${request.action}`);
134
+ if (request.action === 'browser_release') {
135
+ const found = (await runtime.listBrowsers()).find(browser => browser.browserId === browserId);
136
+ if (found?.operations.includes('detach')) {
137
+ return runtime.detach({ browserId }, context);
138
+ }
155
139
  }
156
- touchProviderBrowser(providerBrowserKey(options.descriptor.providerId, browserId));
157
- return options.handleAction({
140
+ touchProviderBrowser(providerBrowserKey(descriptor.providerId, browserId));
141
+ return runtime.invokeAction({
158
142
  browserId,
159
143
  action: request.action,
160
144
  data: request.data ?? {},
161
145
  controlSource: request.controlSource,
162
146
  });
163
147
  }
164
- function assertPairing(status, pairing, browserId) {
165
- if (status === 'pairing' &&
166
- (pairing?.kind !== 'qr' || typeof pairing.url !== 'string' || !pairing.url.trim())) {
167
- throw new Error(`Pairing browser ${browserId} needs a pairing URL`);
168
- }
169
- if (status !== 'pairing' && pairing) {
170
- throw new Error(`Pairing metadata requires status "pairing": ${browserId}`);
171
- }
172
- if (pairing &&
173
- ((pairing.code !== undefined && typeof pairing.code !== 'string') ||
174
- (pairing.label !== undefined && typeof pairing.label !== 'string'))) {
175
- throw new Error(`Provider browser ${browserId} has invalid pairing metadata`);
176
- }
177
- }
178
148
  function handleConnection(socket) {
179
149
  connections.add(socket);
180
150
  socket.once('close', () => connections.delete(socket));
@@ -216,22 +186,22 @@ export function createProviderServer(options) {
216
186
  }
217
187
  return {
218
188
  socketPath,
189
+ runtime,
219
190
  async start() {
220
191
  if (state !== 'idle')
221
192
  throw new Error(`Provider server is already ${state}`);
222
- assertDescriptor();
223
- const providerId = options.descriptor.providerId;
193
+ const providerId = descriptor.providerId;
224
194
  if (activeProviderIds.has(providerId)) {
225
195
  throw new Error(`Provider ${providerId} is already running in this process`);
226
196
  }
227
197
  state = 'starting';
228
198
  activeProviderIds.add(providerId);
229
199
  const startToken = process.env[PROVIDER_START_TOKEN_ENV] || randomUUID();
230
- const existing = readLiveProvider(options.descriptor.providerId);
200
+ const existing = readLiveProvider(descriptor.providerId);
231
201
  if (existing) {
232
202
  state = 'idle';
233
203
  activeProviderIds.delete(providerId);
234
- throw new Error(`Provider ${options.descriptor.providerId} is already running (pid ${existing.pid})`);
204
+ throw new Error(`Provider ${descriptor.providerId} is already running (pid ${existing.pid})`);
235
205
  }
236
206
  if (!claimProviderStart(providerId, startToken)) {
237
207
  state = 'idle';
@@ -256,7 +226,7 @@ export function createProviderServer(options) {
256
226
  });
257
227
  if (process.platform !== 'win32')
258
228
  chmodSync(socketPath, 0o600);
259
- publishLiveProvider(options.descriptor, socketPath);
229
+ publishLiveProvider(descriptor, socketPath);
260
230
  releaseProviderStart(providerId, startToken);
261
231
  state = 'running';
262
232
  }
@@ -281,9 +251,9 @@ export function createProviderServer(options) {
281
251
  updateBrowsers(nextBrowsers) {
282
252
  if (state !== 'running')
283
253
  throw new Error('Provider server is not running');
284
- assertBrowsers(nextBrowsers);
254
+ runtime.updateBrowsers(nextBrowsers);
285
255
  browsers = [...nextBrowsers];
286
- browserIds = publishProviderBrowsers(options.descriptor, socketPath, browsers, browserIds);
256
+ browserIds = publishProviderBrowsers(descriptor, socketPath, browsers, browserIds);
287
257
  },
288
258
  touchBrowser(browserId) {
289
259
  if (state !== 'running')
@@ -291,7 +261,7 @@ export function createProviderServer(options) {
291
261
  if (!browsers.some(browser => browser.browserId === browserId)) {
292
262
  throw new Error(`Provider browser not found: ${browserId}`);
293
263
  }
294
- touchProviderBrowser(providerBrowserKey(options.descriptor.providerId, browserId));
264
+ touchProviderBrowser(providerBrowserKey(descriptor.providerId, browserId));
295
265
  },
296
266
  async stop() {
297
267
  if (state === 'idle')
@@ -314,9 +284,9 @@ export function createProviderServer(options) {
314
284
  browsers = [];
315
285
  if (process.platform !== 'win32')
316
286
  rmSync(socketPath, { force: true });
317
- unpublishLiveProvider(options.descriptor.providerId);
287
+ unpublishLiveProvider(descriptor.providerId);
318
288
  })().finally(() => {
319
- activeProviderIds.delete(options.descriptor.providerId);
289
+ activeProviderIds.delete(descriptor.providerId);
320
290
  stopPromise = null;
321
291
  state = 'idle';
322
292
  });