@mearl/provider 3.0.0-beta.0 → 3.0.0-beta.10

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.
@@ -0,0 +1,55 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { PROVIDER_APP_RESOURCE_PROTOCOL_VERSION, MAX_PROVIDER_APP_RESOURCE_MESSAGE_BYTES, appProfileRevision, validateAppProfileDefinition, requireReadyProviderEnhancementTarget, BROWSER_DEVICE_PRESET_NAMES, } from '@mearl/browser-core';
3
+ import { providerBrowserKey } from './registry.js';
4
+ import { requestProviderHost } from './control-rpc.js';
5
+ /** Reads accepted installed resources. It never installs code in a page or runs host services. */
6
+ export function createProviderAppResourceClient(providerId, providerBrowserId) {
7
+ if (!providerId || !providerBrowserId)
8
+ throw new Error('Provider and browser IDs are required');
9
+ const target = providerBrowserKey(providerId, providerBrowserId);
10
+ async function request(data, deadline = Date.now() + 60_000) {
11
+ const result = await requestProviderHost(target, '__provider_app_resources', data, {
12
+ deadline, maximumBytes: MAX_PROVIDER_APP_RESOURCE_MESSAGE_BYTES,
13
+ assertHost(host) {
14
+ if (host.appResourceProtocolVersion !== PROVIDER_APP_RESOURCE_PROTOCOL_VERSION)
15
+ throw new Error('PROVIDER_APP_RESOURCES_UNSUPPORTED: update Mearl and restart the Provider control host');
16
+ },
17
+ });
18
+ for (const revision of [result.capabilityRevision, result.installationRevision])
19
+ if (revision !== null && (typeof revision !== 'string' || !/^[a-f0-9]{64}$/.test(revision)))
20
+ throw new Error('Invalid App resource revision');
21
+ return result;
22
+ }
23
+ return {
24
+ async catalog(params = {}, deadline) {
25
+ const result = await request({ operation: 'catalog', params }, deadline);
26
+ if (!('catalog' in result) || !result.catalog || !['available', 'partial', 'unsupported'].includes(result.catalog.status) ||
27
+ !Array.isArray(result.catalog.entries) || !Array.isArray(result.catalog.issues))
28
+ throw new Error('Invalid App resource catalog');
29
+ return result;
30
+ },
31
+ async resolve(data, deadline) {
32
+ if (!Number.isSafeInteger(data.providerTabId) || data.providerTabId < 0 || typeof data.profile !== 'string' || !data.profile || data.profile.length > 161)
33
+ throw new Error('Invalid App resource request');
34
+ const result = await request({ ...data, operation: 'resolve' }, deadline);
35
+ if (!('config' in result) || result.config?.binding?.version !== 1 || result.config.profile !== data.profile ||
36
+ typeof result.config.userAgent !== 'string' || !BROWSER_DEVICE_PRESET_NAMES.includes(result.recommendedPreset))
37
+ throw new Error('Invalid resolved App config');
38
+ const definition = validateAppProfileDefinition(result.config.binding.definition);
39
+ if (definition.name !== data.profile || result.config.binding.revision !== appProfileRevision(definition))
40
+ throw new Error('Invalid App definition revision');
41
+ requireReadyProviderEnhancementTarget(result.target, data.providerTabId);
42
+ const binding = result.config.pageRuntime;
43
+ if (binding?.id !== definition.pageRuntime?.id || binding?.sha256 !== definition.pageRuntime?.sha256)
44
+ throw new Error('App runtime binding mismatch');
45
+ if (binding) {
46
+ if (!result.runtime || result.runtime.binding.id !== binding.id || result.runtime.binding.sha256 !== binding.sha256 ||
47
+ typeof result.runtime.source !== 'string' || createHash('sha256').update(result.runtime.source).digest('hex') !== binding.sha256)
48
+ throw new Error('App runtime integrity check failed');
49
+ }
50
+ else if (result.runtime || definition.bridges.length || Object.keys(definition.globals).length)
51
+ throw new Error('Required App runtime is missing');
52
+ return result;
53
+ },
54
+ };
55
+ }
@@ -1,10 +1,20 @@
1
+ import type { BrowserControlSessionMode } from '@mearl/browser-core';
1
2
  import { type DaemonRecord } from '@mearl/daemon-core';
2
3
  export interface ProviderControlHostRecord extends DaemonRecord {
3
4
  socketPath: string;
4
5
  token: string;
5
6
  ready: boolean;
6
7
  protocolVersion: number;
8
+ /** Missing on older hosts, which only support Mearl sessions. */
9
+ controlSessionModes?: readonly BrowserControlSessionMode[];
10
+ enhancementProtocolVersion?: number;
11
+ enhancementNavigationOriginVersion?: number;
12
+ enhancementPreparationVersion?: number;
13
+ enhancementHostEnvironmentVersion?: number;
14
+ /** RPC support only; each read still prepares and validates the accepted App resources. */
15
+ appResourceProtocolVersion?: number;
7
16
  }
17
+ export declare function assertProviderControlSessionMode(host: Pick<ProviderControlHostRecord, 'controlSessionModes'> | null, mode?: BrowserControlSessionMode): void;
8
18
  export declare function providerControlHostFile(): string;
9
19
  export declare function providerControlSocketPath(pid: number): string;
10
20
  export declare function readProviderControlHost(): ProviderControlHostRecord | null;
@@ -4,6 +4,11 @@ import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { assertProviderProtocolVersion, MEARL_PROVIDER_PROTOCOL_VERSION } from './protocol.js';
6
6
  import { isAlive, keyedConfigFile, claimRecord, readRecord, removeInvalidRecord, removeRecordIfPid, writeRecordIfPid, } from '@mearl/daemon-core';
7
+ export function assertProviderControlSessionMode(host, mode = 'mearl') {
8
+ if (mode === 'external' && (!Array.isArray(host?.controlSessionModes) || !host.controlSessionModes.includes('external'))) {
9
+ throw new Error('CONTROL_SESSION_MODE_UNSUPPORTED: the Provider requires external task control. Update Mearl and restart the Provider control host.');
10
+ }
11
+ }
7
12
  export function providerControlHostFile() {
8
13
  return keyedConfigFile('control', 'providers');
9
14
  }
@@ -0,0 +1,7 @@
1
+ import { type ProviderControlHostRecord } from './control-host.js';
2
+ /** Package-private transport shared by the two closed Provider host services. */
3
+ export declare function requestProviderHost<T>(providerTarget: string, action: '__provider_enhancement' | '__provider_app_resources', data: unknown, options: {
4
+ deadline: number;
5
+ maximumBytes: number;
6
+ assertHost(host: ProviderControlHostRecord): void;
7
+ }): Promise<T>;
@@ -0,0 +1,66 @@
1
+ import net from 'node:net';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { ensureProviderControlHost } from './control-host.js';
4
+ import { MEARL_PROVIDER_PROTOCOL_VERSION } from './protocol.js';
5
+ /** Package-private transport shared by the two closed Provider host services. */
6
+ export async function requestProviderHost(providerTarget, action, data, options) {
7
+ const remaining = () => {
8
+ const ms = options.deadline - Date.now();
9
+ if (!Number.isSafeInteger(options.deadline) || ms < 1 || ms > 2_147_483_647)
10
+ throw new Error('Provider host deadline exceeded or invalid');
11
+ return ms;
12
+ };
13
+ const host = await ensureProviderControlHost(Math.min(remaining(), 10_000));
14
+ options.assertHost(host);
15
+ const id = randomUUID();
16
+ const line = JSON.stringify({ id, action, providerTarget, protocolVersion: MEARL_PROVIDER_PROTOCOL_VERSION,
17
+ controlSource: 'local', deadline: options.deadline, data }) + '\n';
18
+ if (Buffer.byteLength(line) > options.maximumBytes)
19
+ throw new Error('Provider host request exceeded its byte limit');
20
+ const waitMs = remaining();
21
+ return new Promise((resolve, reject) => {
22
+ const socket = net.createConnection(host.socketPath);
23
+ socket.setEncoding('utf8');
24
+ let buffer = '', settled = false;
25
+ const finish = (error, result) => {
26
+ if (settled)
27
+ return;
28
+ settled = true;
29
+ clearTimeout(timer);
30
+ socket.removeAllListeners();
31
+ socket.destroy();
32
+ if (error)
33
+ reject(error);
34
+ else
35
+ resolve(result);
36
+ };
37
+ const timer = setTimeout(() => finish(new Error('Provider host request timed out; any dispatched effect is unconfirmed')), waitMs);
38
+ socket.on('connect', () => socket.write(line));
39
+ socket.on('data', chunk => {
40
+ buffer += chunk;
41
+ if (Buffer.byteLength(buffer) > options.maximumBytes)
42
+ return finish(new Error('Provider host response exceeded its byte limit'));
43
+ while (buffer.includes('\n')) {
44
+ const index = buffer.indexOf('\n'), line = buffer.slice(0, index);
45
+ buffer = buffer.slice(index + 1);
46
+ let response;
47
+ try {
48
+ response = JSON.parse(line);
49
+ }
50
+ catch {
51
+ return finish(new Error('Invalid Provider host response'));
52
+ }
53
+ if (response?.id !== id)
54
+ continue;
55
+ if (response.success !== true)
56
+ return finish(new Error(response.error ?? 'Provider host request failed'));
57
+ if (!response.data || typeof response.data !== 'object')
58
+ return finish(new Error('Invalid Provider host response'));
59
+ return finish(undefined, response.data);
60
+ }
61
+ });
62
+ socket.on('error', error => finish(error));
63
+ socket.on('end', () => finish(new Error('Provider host connection closed before its response')));
64
+ socket.on('close', () => finish(new Error('Provider host connection closed before its response')));
65
+ });
66
+ }
@@ -0,0 +1,19 @@
1
+ import { type BrowserEnhancementInvocation, type ProviderEnhancementBinding, type ProviderEnhancementPreparation, type ProviderEnhancementRequest } from '@mearl/browser-core';
2
+ export interface ProviderEnhancementClient {
3
+ inspect(request: Omit<Extract<ProviderEnhancementRequest, {
4
+ operation: 'inspect';
5
+ }>, 'operation'>, deadline?: number): Promise<ProviderEnhancementPreparation>;
6
+ bind(request: Omit<Extract<ProviderEnhancementRequest, {
7
+ operation: 'bind';
8
+ }>, 'operation'>, deadline?: number): Promise<ProviderEnhancementBinding>;
9
+ invoke(binding: ProviderEnhancementBinding, invocation: BrowserEnhancementInvocation, deadline?: number): Promise<unknown>;
10
+ release(binding: ProviderEnhancementBinding, deadline?: number): Promise<{
11
+ released: boolean;
12
+ conflicts?: string[];
13
+ }>;
14
+ }
15
+ /** Uses the accepted instance's narrow control-host gateway; never forwards ordinary actions. */
16
+ export declare function createProviderEnhancementClient(providerId: string, providerBrowserId: string, options?: {
17
+ timeoutMs?: number;
18
+ requireHostEnvironment?: boolean;
19
+ }): ProviderEnhancementClient;
@@ -0,0 +1,100 @@
1
+ import { BROWSER_ENHANCEMENT_PROTOCOL_VERSION, BROWSER_ENHANCEMENT_NAVIGATION_ORIGIN_VERSION, BROWSER_ENHANCEMENT_PREPARATION_VERSION, isBrowserEnhancementPreparationUrl, BROWSER_ENHANCEMENT_HOST_ENVIRONMENT_VERSION, isBrowserEnhancementAction, isEnhancementNavigationOrigin, requiredBrowserEnhancementActions, } from '@mearl/browser-core';
2
+ import { requestProviderHost } from './control-rpc.js';
3
+ import { providerBrowserKey } from './registry.js';
4
+ /** Uses the accepted instance's narrow control-host gateway; never forwards ordinary actions. */
5
+ export function createProviderEnhancementClient(providerId, providerBrowserId, options = {}) {
6
+ if (!providerId || !providerBrowserId)
7
+ throw new Error('Provider and browser IDs are required');
8
+ const timeoutMs = options.timeoutMs ?? 60_000;
9
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 300_000)
10
+ throw new Error('Enhancement timeout must be between 1 and 300000 milliseconds');
11
+ const providerTarget = providerBrowserKey(providerId, providerBrowserId);
12
+ const uncertain = new Set();
13
+ async function request(data, callerDeadline) {
14
+ return requestProviderHost(providerTarget, '__provider_enhancement', data, {
15
+ deadline: callerDeadline ?? Date.now() + timeoutMs, maximumBytes: 1024 * 1024,
16
+ assertHost(host) {
17
+ if (host.enhancementProtocolVersion !== BROWSER_ENHANCEMENT_PROTOCOL_VERSION)
18
+ throw new Error('BROWSER_ENHANCEMENT_UNSUPPORTED: update Mearl and restart the Provider control host');
19
+ // Support negotiation only. The Creator's verified initial environment decides the policy.
20
+ if (options.requireHostEnvironment && host.enhancementHostEnvironmentVersion !== BROWSER_ENHANCEMENT_HOST_ENVIRONMENT_VERSION)
21
+ throw new Error('BROWSER_ENHANCEMENT_UNSUPPORTED: the control host cannot preserve the captured host environment');
22
+ if (data.operation === 'inspect' && host.enhancementPreparationVersion !== BROWSER_ENHANCEMENT_PREPARATION_VERSION)
23
+ throw new Error('BROWSER_ENHANCEMENT_UNSUPPORTED: the control host cannot negotiate a preparation page');
24
+ if ((data.operation === 'bind' || data.operation === 'inspect') && data.navigationOrigin !== undefined &&
25
+ host.enhancementNavigationOriginVersion !== BROWSER_ENHANCEMENT_NAVIGATION_ORIGIN_VERSION)
26
+ throw new Error('BROWSER_ENHANCEMENT_UNSUPPORTED: the control host cannot authorize a first navigation origin');
27
+ },
28
+ });
29
+ }
30
+ function assertConfirmed(binding) {
31
+ if (uncertain.has(binding.bindingId))
32
+ throw new Error('ENVIRONMENT_UNCONFIRMED: reverify and rebind before another mutation');
33
+ }
34
+ return {
35
+ async inspect(data, deadline) {
36
+ if (!Number.isSafeInteger(data.providerTabId) || data.providerTabId < 0 || !isEnhancementNavigationOrigin(data.navigationOrigin) ||
37
+ !Array.isArray(data.actions) || !data.actions.length || data.actions.some(action => !isBrowserEnhancementAction(action)))
38
+ throw new Error('Invalid enhancement inspection request');
39
+ const result = await request({ ...data, operation: 'inspect' }, deadline);
40
+ if (!('preparation' in result) || result.preparation.providerTabId !== data.providerTabId ||
41
+ !Number.isSafeInteger(result.preparation.tabId) || result.preparation.tabId < 0 ||
42
+ typeof result.preparation.targetId !== 'string' || !result.preparation.targetId || result.preparation.targetId.length > 256 ||
43
+ typeof result.preparation.nativeBrowserId !== 'string' || !result.preparation.nativeBrowserId ||
44
+ (data.nativeBrowserId !== undefined && result.preparation.nativeBrowserId !== data.nativeBrowserId) ||
45
+ !isBrowserEnhancementPreparationUrl(result.preparation.url) ||
46
+ result.preparation.capabilities?.protocolVersion !== BROWSER_ENHANCEMENT_PROTOCOL_VERSION ||
47
+ result.preparation.capabilities?.preparationVersion !== BROWSER_ENHANCEMENT_PREPARATION_VERSION ||
48
+ !Array.isArray(result.preparation.capabilities.actions) || data.actions.some(action => !result.preparation.capabilities.actions.includes(action)))
49
+ throw new Error('Invalid enhancement preparation response');
50
+ return result.preparation;
51
+ },
52
+ async bind(data, deadline) {
53
+ if (!Number.isSafeInteger(data.providerTabId) || data.providerTabId < 0 || !Array.isArray(data.actions) ||
54
+ (data.navigationOrigin !== undefined && !isEnhancementNavigationOrigin(data.navigationOrigin)) ||
55
+ !data.actions.length || data.actions.some(action => !isBrowserEnhancementAction(action)))
56
+ throw new Error('Invalid enhancement binding request');
57
+ const result = await request({ ...data, operation: 'bind' }, deadline);
58
+ if (!('binding' in result) || typeof result.binding?.bindingId !== 'string' || !result.binding.bindingId ||
59
+ !Number.isSafeInteger(result.binding.generation) || result.binding.generation < 1 ||
60
+ !Number.isSafeInteger(result.binding.tabId) || result.binding.tabId < 0 ||
61
+ typeof result.binding.mainFrameId !== 'string' || !result.binding.mainFrameId ||
62
+ (result.binding.targetId !== undefined && (typeof result.binding.targetId !== 'string' || !result.binding.targetId || result.binding.targetId.length > 256)) ||
63
+ typeof result.binding.nativeBrowserId !== 'string' || !result.binding.nativeBrowserId ||
64
+ (data.nativeBrowserId !== undefined && result.binding.nativeBrowserId !== data.nativeBrowserId) ||
65
+ result.binding.providerTabId !== data.providerTabId || !Array.isArray(result.binding.actions) ||
66
+ data.actions.some(action => !result.binding.actions.includes(action)))
67
+ throw new Error('Invalid enhancement binding response');
68
+ uncertain.delete(result.binding.bindingId);
69
+ return result.binding;
70
+ },
71
+ async invoke(binding, invocation, deadline) {
72
+ assertConfirmed(binding);
73
+ if (!isBrowserEnhancementAction(invocation.action) || requiredBrowserEnhancementActions(invocation).some(action => !binding.actions.includes(action)))
74
+ throw new Error('BROWSER_ENHANCEMENT_SCOPE_MISMATCH');
75
+ try {
76
+ const result = await request({ ...invocation, operation: 'invoke', bindingId: binding.bindingId, generation: binding.generation }, deadline);
77
+ if (!('result' in result))
78
+ throw new Error('Invalid enhancement invocation response');
79
+ return result.result;
80
+ }
81
+ catch (error) {
82
+ uncertain.add(binding.bindingId);
83
+ throw error;
84
+ }
85
+ },
86
+ async release(binding, deadline) {
87
+ assertConfirmed(binding);
88
+ try {
89
+ const result = await request({ operation: 'release', bindingId: binding.bindingId, generation: binding.generation }, deadline);
90
+ if (!('released' in result) || typeof result.released !== 'boolean')
91
+ throw new Error('Invalid enhancement release response');
92
+ return result;
93
+ }
94
+ catch (error) {
95
+ uncertain.add(binding.bindingId);
96
+ throw error;
97
+ }
98
+ },
99
+ };
100
+ }
package/dist/host.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { listInstalledProviders, validateInstalledProviderManifest, registerProviderPackage, stopProvider, readLiveProvider, readProviderStart, uninstallProvider, } from './registry.js';
2
2
  export type { InstalledProviderManifest, ProviderPackageMetadata } from './types.js';
3
- export { ensureProviderControlHost, readProviderControlHost, providerControlHostFile, providerControlSocketPath, } from './control-host.js';
3
+ export { ensureProviderControlHost, assertProviderControlSessionMode, readProviderControlHost, providerControlHostFile, providerControlSocketPath, } from './control-host.js';
4
4
  export type { ProviderControlHostRecord } from './control-host.js';
package/dist/host.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export { listInstalledProviders, validateInstalledProviderManifest, registerProviderPackage, stopProvider, readLiveProvider, readProviderStart, uninstallProvider, } from './registry.js';
2
- export { ensureProviderControlHost, readProviderControlHost, providerControlHostFile, providerControlSocketPath, } from './control-host.js';
2
+ export { ensureProviderControlHost, assertProviderControlSessionMode, readProviderControlHost, providerControlHostFile, providerControlSocketPath, } from './control-host.js';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from './types.js';
2
+ export { createProviderEnhancementClient, type ProviderEnhancementClient } from './enhancements.js';
3
+ export { createProviderAppResourceClient, type ProviderAppResourceClient } from './app-resources.js';
2
4
  export { assertProviderProtocolVersion } from './protocol.js';
3
5
  export { PROVIDER_ACTIONS, isProviderAction } from './actions.js';
4
6
  export { providerDataDir } from './registry.js';
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from './types.js';
2
+ export { createProviderEnhancementClient } from './enhancements.js';
3
+ export { createProviderAppResourceClient } from './app-resources.js';
2
4
  export { assertProviderProtocolVersion } from './protocol.js';
3
5
  export { PROVIDER_ACTIONS, isProviderAction } from './actions.js';
4
6
  export { providerDataDir } from './registry.js';
package/dist/launcher.js CHANGED
@@ -95,17 +95,28 @@ export async function ensureInstalledProvidersRunning(providerIds) {
95
95
  }
96
96
  if (starting.size === 0)
97
97
  return warnings;
98
- const deadline = Date.now() + 2_000;
99
- let remaining = [...starting.keys()];
100
- while (remaining.length > 0 && Date.now() < deadline) {
98
+ const startupTimeout = (providerId) => installed.find(manifest => manifest.providerId === providerId)?.browserType.controlSessionModes?.includes('external')
99
+ ? 12_000 : 2_000;
100
+ const deadlines = new Map([...starting.keys()].map(id => [id, Date.now() + startupTimeout(id)]));
101
+ const remaining = new Set(starting.keys());
102
+ const waiting = new Set(starting.keys());
103
+ while (waiting.size > 0) {
101
104
  await delay(100);
102
105
  const current = new Set(listLiveProviders().map(record => record.providerId));
103
- remaining = remaining.filter(providerId => !current.has(providerId));
106
+ for (const providerId of waiting) {
107
+ if (current.has(providerId)) {
108
+ waiting.delete(providerId);
109
+ remaining.delete(providerId);
110
+ }
111
+ else if (Date.now() >= deadlines.get(providerId)) {
112
+ waiting.delete(providerId);
113
+ }
114
+ }
104
115
  }
105
116
  for (const [providerId, token] of starting) {
106
- if (!remaining.includes(providerId) && token)
117
+ if (!remaining.has(providerId) && token)
107
118
  releaseProviderStart(providerId, token);
108
- if (!remaining.includes(providerId)) {
119
+ if (!remaining.has(providerId)) {
109
120
  try {
110
121
  hasCompatibleLiveProvider(providerId);
111
122
  }
@@ -116,7 +127,7 @@ export async function ensureInstalledProvidersRunning(providerIds) {
116
127
  }
117
128
  for (const providerId of remaining) {
118
129
  const logFile = providerLogFile(providerId);
119
- warnings.push(`provider ${providerId}: did not become ready within 2s${existsSync(logFile) ? `; see ${logFile}` : ''}`);
130
+ warnings.push(`provider ${providerId}: did not become ready within ${startupTimeout(providerId) / 1000}s${existsSync(logFile) ? `; see ${logFile}` : ''}`);
120
131
  }
121
132
  // A ready provider may still be publishing its first browser inventory.
122
133
  await delay(100);
@@ -1,2 +1,4 @@
1
+ import type { BrowserControlSessionMode } from '@mearl/browser-core';
1
2
  export declare const MEARL_PROVIDER_PROTOCOL_VERSION = 3;
3
+ export declare function isProviderControlSessionModes(value: unknown): value is BrowserControlSessionMode[];
2
4
  export declare function assertProviderProtocolVersion(actual: unknown, peer?: string): void;
package/dist/protocol.js CHANGED
@@ -1,4 +1,9 @@
1
1
  export const MEARL_PROVIDER_PROTOCOL_VERSION = 3;
2
+ export function isProviderControlSessionModes(value) {
3
+ return Array.isArray(value) && value.length > 0 &&
4
+ value.every(mode => mode === 'mearl' || mode === 'external') &&
5
+ new Set(value).size === value.length;
6
+ }
2
7
  export function assertProviderProtocolVersion(actual, peer = 'Provider') {
3
8
  if (actual !== MEARL_PROVIDER_PROTOCOL_VERSION) {
4
9
  throw new Error(`PROVIDER_PROTOCOL_MISMATCH: ${peer} uses protocol ${String(actual ?? 'unknown')}; expected ${MEARL_PROVIDER_PROTOCOL_VERSION}. Update Mearl and the Provider package, then restart the services.`);
@@ -1,5 +1,7 @@
1
1
  import { MEARL_PROVIDER_PROTOCOL_VERSION } from './types.js';
2
2
  import { isProviderAction } from './actions.js';
3
+ import { isProviderControlSessionModes } from './protocol.js';
4
+ import { isProviderEnhancementTarget } from '@mearl/browser-core';
3
5
  const LIFECYCLE_OPERATIONS = new Set([
4
6
  'start',
5
7
  'close',
@@ -8,6 +10,10 @@ const LIFECYCLE_OPERATIONS = new Set([
8
10
  'access',
9
11
  'syncCookies',
10
12
  ]);
13
+ function assertDeadline(context) {
14
+ if (context.deadline !== undefined && (!Number.isSafeInteger(context.deadline) || Date.now() >= context.deadline))
15
+ throw new Error('Provider request deadline exceeded');
16
+ }
11
17
  function isJsonValue(value, depth = 0) {
12
18
  if (depth > 32)
13
19
  return false;
@@ -329,6 +335,13 @@ export function validateProviderBrowser(browser) {
329
335
  if (!['native', 'reconstructed', 'none'].includes(capabilities.screenshot)) {
330
336
  throw new Error(`Provider browser ${browser.browserId} has invalid screenshot capabilities`);
331
337
  }
338
+ if (capabilities.controlSessionMode !== undefined &&
339
+ !['mearl', 'external'].includes(capabilities.controlSessionMode)) {
340
+ throw new Error(`Provider browser ${browser.browserId} has invalid control session mode`);
341
+ }
342
+ if (browser.control.kind === 'host' && capabilities.controlSessionMode === 'external') {
343
+ throw new Error('External control sessions require Provider control');
344
+ }
332
345
  const supportsScreenshot = capabilities.actions.includes('page_screenshot');
333
346
  if (supportsScreenshot === (capabilities.screenshot === 'none')) {
334
347
  throw new Error(`Provider browser ${browser.browserId} has inconsistent screenshot capabilities`);
@@ -390,6 +403,9 @@ function validateDefinition(definition) {
390
403
  }
391
404
  if (!definition.browserType.name?.trim())
392
405
  throw new Error('provider browser type name is required');
406
+ if (definition.browserType.controlSessionModes !== undefined &&
407
+ !isProviderControlSessionModes(definition.browserType.controlSessionModes))
408
+ throw new Error('Provider control session modes are invalid');
393
409
  if (!isProviderInputSchema(definition.browserType.inputSchema) ||
394
410
  definition.browserType.inputSchema.type !== 'object') {
395
411
  throw new Error('Provider input schema is invalid');
@@ -454,8 +470,24 @@ export function validateProviderLaunchForm(form) {
454
470
  export function createProviderRuntime(definition) {
455
471
  validateDefinition(definition);
456
472
  let publishedBrowsers = [];
473
+ const declaredModes = new Set(definition.browserType.controlSessionModes ?? ['mearl']);
474
+ const controlModes = new Map();
475
+ const rememberControlMode = (browser) => {
476
+ if (browser.control.kind !== 'none') {
477
+ controlModes.set(browser.browserId, browser.control.capabilities?.controlSessionMode ?? 'mearl');
478
+ }
479
+ };
457
480
  const validateRuntimeBrowser = (browser) => {
458
481
  validateProviderBrowser(browser);
482
+ if (browser.control.kind !== 'none') {
483
+ const previous = controlModes.get(browser.browserId);
484
+ const mode = browser.control.capabilities?.controlSessionMode ?? 'mearl';
485
+ if (!declaredModes.has(mode))
486
+ throw new Error(`Provider did not declare control session mode: ${mode}`);
487
+ if (previous !== undefined && previous !== mode) {
488
+ throw new Error('CONTROL_SESSION_MODE_CHANGED: use a new browserId to change task control mode');
489
+ }
490
+ }
459
491
  const implementations = {
460
492
  start: definition.start,
461
493
  detach: definition.detach,
@@ -470,6 +502,7 @@ export function createProviderRuntime(definition) {
470
502
  }
471
503
  };
472
504
  const listBrowsers = async (context = {}) => {
505
+ assertDeadline(context);
473
506
  const browsers = definition.listBrowsers
474
507
  ? await definition.listBrowsers(context)
475
508
  : publishedBrowsers;
@@ -481,6 +514,7 @@ export function createProviderRuntime(definition) {
481
514
  }
482
515
  seen.add(browser.browserId);
483
516
  }
517
+ browsers.forEach(rememberControlMode);
484
518
  return browsers;
485
519
  };
486
520
  const requireBrowser = async (browserId, operation, context) => {
@@ -490,6 +524,7 @@ export function createProviderRuntime(definition) {
490
524
  if (!browser.operations.includes(operation)) {
491
525
  throw new Error(`Provider browser ${browserId} does not support ${operation}`);
492
526
  }
527
+ assertDeadline(context);
493
528
  return browser;
494
529
  };
495
530
  return {
@@ -500,9 +535,11 @@ export function createProviderRuntime(definition) {
500
535
  if (new Set(browsers.map(browser => browser.browserId)).size !== browsers.length) {
501
536
  throw new Error('Provider browser ids must be unique');
502
537
  }
538
+ browsers.forEach(rememberControlMode);
503
539
  publishedBrowsers = [...browsers];
504
540
  },
505
541
  async getLaunchForm(request, context = {}) {
542
+ assertDeadline(context);
506
543
  if (!definition.browserType.dynamicLaunchForm || !definition.getLaunchForm)
507
544
  throw new Error('Provider does not support dynamic launch forms');
508
545
  if (!request ||
@@ -523,9 +560,11 @@ export function createProviderRuntime(definition) {
523
560
  return form;
524
561
  },
525
562
  async launch(options, context = {}) {
563
+ assertDeadline(context);
526
564
  validateProviderInput(definition.browserType.inputSchema, options);
527
565
  const browser = await definition.launch(options, context);
528
566
  validateRuntimeBrowser(browser);
567
+ rememberControlMode(browser);
529
568
  return browser;
530
569
  },
531
570
  async start(request, context = {}) {
@@ -534,6 +573,7 @@ export function createProviderRuntime(definition) {
534
573
  throw new Error('Provider does not implement start');
535
574
  const browser = await definition.start(request, context);
536
575
  validateRuntimeBrowser(browser);
576
+ rememberControlMode(browser);
537
577
  return browser;
538
578
  },
539
579
  async close(request, context = {}) {
@@ -581,6 +621,7 @@ export function createProviderRuntime(definition) {
581
621
  return { ...access, url: url.toString() };
582
622
  },
583
623
  async configure(options, context = {}) {
624
+ assertDeadline(context);
584
625
  const configuration = definition.browserType.configuration;
585
626
  if (!configuration || !definition.configure) {
586
627
  throw new Error('Provider does not support configuration');
@@ -588,8 +629,32 @@ export function createProviderRuntime(definition) {
588
629
  validateProviderInput(configuration.inputSchema, options);
589
630
  return definition.configure(options, context);
590
631
  },
632
+ async getEnhancementTarget(request, context = {}) {
633
+ assertDeadline(context);
634
+ if (!definition.getEnhancementTarget)
635
+ throw new Error('BROWSER_ENHANCEMENT_TARGET_UNSUPPORTED');
636
+ if (!Number.isSafeInteger(request.tabId) || request.tabId < 0)
637
+ throw new Error('A valid Provider tabId is required');
638
+ const browser = (await listBrowsers(context)).find(item => item.browserId === request.browserId);
639
+ if (browser?.status !== 'connected' || browser.control.kind !== 'provider' ||
640
+ browser.control.capabilities.controlSessionMode !== 'external')
641
+ throw new Error('BROWSER_ENHANCEMENT_OWNER_UNAVAILABLE');
642
+ assertDeadline(context);
643
+ const target = await definition.getEnhancementTarget(request, context);
644
+ if (!isProviderEnhancementTarget(target) || target.tabId !== request.tabId)
645
+ throw new Error('Invalid Provider enhancement target');
646
+ assertDeadline(context);
647
+ return target.ready
648
+ ? { tabId: target.tabId, ready: true, ...(target.mainFrameId ? { mainFrameId: target.mainFrameId } : {}),
649
+ ...(target.targetId ? { targetId: target.targetId } : {}), url: target.url,
650
+ ...(target.initialEnvironment ? { initialEnvironment: structuredClone(target.initialEnvironment) } : {}),
651
+ }
652
+ : { tabId: target.tabId, ready: false };
653
+ },
591
654
  async invokeAction(actionContext) {
592
- const browser = (await listBrowsers({ controlSource: actionContext.controlSource })).find(item => item.browserId === actionContext.browserId);
655
+ const browser = (await listBrowsers({ controlSource: actionContext.controlSource,
656
+ ...(actionContext.deadline !== undefined ? { deadline: actionContext.deadline } : {}),
657
+ })).find(item => item.browserId === actionContext.browserId);
593
658
  if (!browser)
594
659
  throw new Error(`Provider browser not found: ${actionContext.browserId}`);
595
660
  if (browser.status !== 'connected') {
@@ -603,6 +668,7 @@ export function createProviderRuntime(definition) {
603
668
  }
604
669
  if (!definition.handleAction)
605
670
  throw new Error('Provider does not implement browser actions');
671
+ assertDeadline(actionContext);
606
672
  return definition.handleAction(actionContext);
607
673
  },
608
674
  };
package/dist/registry.js CHANGED
@@ -4,7 +4,7 @@ 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
6
  import { validateProviderLaunchForm, isProviderInputSchema } from './provider-runtime.js';
7
- import { assertProviderProtocolVersion } from './protocol.js';
7
+ import { assertProviderProtocolVersion, isProviderControlSessionModes } from './protocol.js';
8
8
  export const INSTALLED_PROVIDERS_SUBDIR = 'providers/installed';
9
9
  export const LIVE_PROVIDERS_SUBDIR = 'providers/live';
10
10
  export const STARTING_PROVIDERS_SUBDIR = 'providers/starting';
@@ -149,6 +149,8 @@ function isProviderBrowserType(value) {
149
149
  const browserType = value;
150
150
  if (typeof browserType.name !== 'string' || !browserType.name.trim())
151
151
  return false;
152
+ if (browserType.controlSessionModes !== undefined && !isProviderControlSessionModes(browserType.controlSessionModes))
153
+ return false;
152
154
  if (browserType.documentationUrl !== undefined &&
153
155
  (typeof browserType.documentationUrl !== 'string' ||
154
156
  !/^https?:\/\//.test(browserType.documentationUrl)))