@mearl/provider 3.0.0-beta.1 → 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.
- package/README.md +147 -102
- package/README.zh-CN.md +248 -0
- package/dist/app-resources.d.ts +15 -0
- package/dist/app-resources.js +55 -0
- package/dist/control-host.d.ts +10 -0
- package/dist/control-host.js +5 -0
- package/dist/control-rpc.d.ts +7 -0
- package/dist/control-rpc.js +66 -0
- package/dist/enhancements.d.ts +19 -0
- package/dist/enhancements.js +100 -0
- package/dist/host.d.ts +1 -1
- package/dist/host.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/launcher.js +18 -7
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.js +5 -0
- package/dist/provider-runtime.js +67 -1
- package/dist/registry.js +3 -1
- package/dist/server.js +44 -6
- package/dist/types.d.ts +16 -3
- package/package.json +5 -5
|
@@ -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
|
+
}
|
package/dist/control-host.d.ts
CHANGED
|
@@ -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;
|
package/dist/control-host.js
CHANGED
|
@@ -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
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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.
|
|
117
|
+
if (!remaining.has(providerId) && token)
|
|
107
118
|
releaseProviderStart(providerId, token);
|
|
108
|
-
if (!remaining.
|
|
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
|
|
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);
|
package/dist/protocol.d.ts
CHANGED
|
@@ -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.`);
|
package/dist/provider-runtime.js
CHANGED
|
@@ -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
|
|
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)))
|