@crosshands/platform-windows 0.1.2
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/LICENSE +21 -0
- package/NOTICE.md +7 -0
- package/assets/crosshands-pipe-relay.exe +0 -0
- package/assets/payload.json +13 -0
- package/assets/runtime.ps1 +1426 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.d.ts +51 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +547 -0
- package/dist/provider.js.map +1 -0
- package/dist/security.d.ts +25 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +27 -0
- package/dist/security.js.map +1 -0
- package/dist/windows-control-security.d.ts +95 -0
- package/dist/windows-control-security.d.ts.map +1 -0
- package/dist/windows-control-security.js +432 -0
- package/dist/windows-control-security.js.map +1 -0
- package/package.json +46 -0
- package/src/index.ts +76 -0
- package/src/provider.ts +646 -0
- package/src/security.ts +67 -0
- package/src/windows-control-security.ts +562 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ComputerOperationName, type ReferenceBindings } from '@crosshands/contract';
|
|
2
|
+
import type { BrokerEndpoint, LocalControlIdentity, LocalControlRequest, StableAppIdentity } from '@crosshands/runtime';
|
|
3
|
+
import { WindowsComputerProvider } from './provider.js';
|
|
4
|
+
import { type WindowsRelayControlServer } from './windows-control-security.js';
|
|
5
|
+
export declare const packageVersion: "0.1.2";
|
|
6
|
+
export * from './provider.js';
|
|
7
|
+
export * from './security.js';
|
|
8
|
+
export * from './windows-control-security.js';
|
|
9
|
+
type PlatformControlServerOptions = {
|
|
10
|
+
endpoint: BrokerEndpoint;
|
|
11
|
+
identity: LocalControlIdentity;
|
|
12
|
+
handler: (request: LocalControlRequest) => Promise<unknown>;
|
|
13
|
+
};
|
|
14
|
+
export declare function createControlServer(options: PlatformControlServerOptions): Promise<WindowsRelayControlServer>;
|
|
15
|
+
export declare function createProvider(): WindowsComputerProvider;
|
|
16
|
+
export declare function inspectTarget(operation: ComputerOperationName, input: unknown): Promise<{
|
|
17
|
+
bindings: ReferenceBindings;
|
|
18
|
+
appIdentity: StableAppIdentity;
|
|
19
|
+
} | null>;
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,KAAK,EACV,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AACvD,OAAO,EAEL,KAAK,yBAAyB,EAC/B,MAAM,+BAA+B,CAAA;AAEtC,eAAO,MAAM,cAAc,SAA4B,CAAA;AAEvD,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,+BAA+B,CAAA;AAK7C,KAAK,4BAA4B,GAAG;IAClC,QAAQ,EAAE,cAAc,CAAA;IACxB,QAAQ,EAAE,oBAAoB,CAAA;IAC9B,OAAO,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAC5D,CAAA;AAED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,yBAAyB,CAAC,CAoBpC;AAID,wBAAgB,cAAc,IAAI,uBAAuB,CAGxD;AAED,wBAAsB,aAAa,CACjC,SAAS,EAAE,qBAAqB,EAChC,KAAK,EAAE,OAAO,GACb,OAAO,CAAC;IAAE,QAAQ,EAAE,iBAAiB,CAAC;IAAC,WAAW,EAAE,iBAAiB,CAAA;CAAE,GAAG,IAAI,CAAC,CAKjF"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { CONTRACT_VERSIONS } from '@crosshands/contract';
|
|
4
|
+
import { WindowsComputerProvider } from './provider.js';
|
|
5
|
+
import { createWindowsControlServer as createWindowsRelayControlServer } from './windows-control-security.js';
|
|
6
|
+
export const packageVersion = CONTRACT_VERSIONS.product;
|
|
7
|
+
export * from './provider.js';
|
|
8
|
+
export * from './security.js';
|
|
9
|
+
export * from './windows-control-security.js';
|
|
10
|
+
const PAYLOAD_MANIFEST = fileURLToPath(new URL('../assets/payload.json', import.meta.url));
|
|
11
|
+
const CONTROL_RELAY = fileURLToPath(new URL('../assets/crosshands-pipe-relay.exe', import.meta.url));
|
|
12
|
+
export async function createControlServer(options) {
|
|
13
|
+
if (options.endpoint.transport !== 'named-pipe') {
|
|
14
|
+
throw new Error('Windows control relay requires a named-pipe endpoint');
|
|
15
|
+
}
|
|
16
|
+
const manifest = JSON.parse(await readFile(PAYLOAD_MANIFEST, 'utf8'));
|
|
17
|
+
const helperSha256 = manifest.files?.['crosshands-pipe-relay.exe'];
|
|
18
|
+
if (typeof helperSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(helperSha256)) {
|
|
19
|
+
throw new Error('Windows control relay is missing payload hash metadata');
|
|
20
|
+
}
|
|
21
|
+
return createWindowsRelayControlServer({
|
|
22
|
+
identity: options.identity,
|
|
23
|
+
relay: {
|
|
24
|
+
helperPath: CONTROL_RELAY,
|
|
25
|
+
helperSha256,
|
|
26
|
+
pipeName: options.endpoint.address
|
|
27
|
+
},
|
|
28
|
+
handler: options.handler
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
let sharedProvider;
|
|
32
|
+
export function createProvider() {
|
|
33
|
+
sharedProvider = new WindowsComputerProvider();
|
|
34
|
+
return sharedProvider;
|
|
35
|
+
}
|
|
36
|
+
export async function inspectTarget(operation, input) {
|
|
37
|
+
if (sharedProvider === undefined) {
|
|
38
|
+
throw new Error('createProvider() must be called before inspectTarget()');
|
|
39
|
+
}
|
|
40
|
+
return sharedProvider.inspect(operation, input);
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,OAAO,EACL,iBAAiB,EAGlB,MAAM,sBAAsB,CAAA;AAQ7B,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AACvD,OAAO,EACL,0BAA0B,IAAI,+BAA+B,EAE9D,MAAM,+BAA+B,CAAA;AAEtC,MAAM,CAAC,MAAM,cAAc,GAAG,iBAAiB,CAAC,OAAO,CAAA;AAEvD,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,+BAA+B,CAAA;AAE7C,MAAM,gBAAgB,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,wBAAwB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AAC1F,MAAM,aAAa,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,qCAAqC,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AAQpG,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,OAAqC;IAErC,IAAI,OAAO,CAAC,QAAQ,CAAC,SAAS,KAAK,YAAY,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAA;IACzE,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAEnE,CAAA;IACD,MAAM,YAAY,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC,2BAA2B,CAAC,CAAA;IAClE,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;QAC7E,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAA;IAC3E,CAAC;IACD,OAAO,+BAA+B,CAAC;QACrC,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,KAAK,EAAE;YACL,UAAU,EAAE,aAAa;YACzB,YAAY;YACZ,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,OAAO;SACnC;QACD,OAAO,EAAE,OAAO,CAAC,OAAO;KACzB,CAAC,CAAA;AACJ,CAAC;AAED,IAAI,cAAmD,CAAA;AAEvD,MAAM,UAAU,cAAc;IAC5B,cAAc,GAAG,IAAI,uBAAuB,EAAE,CAAA;IAC9C,OAAO,cAAc,CAAA;AACvB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,SAAgC,EAChC,KAAc;IAEd,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAA;IAC3E,CAAC;IACD,OAAO,cAAc,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;AACjD,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type ComputerOperationName, type ComputerProvider, type ProviderHandshake, type ProviderRequest, type ProviderResponse, type ReferenceBindings } from '@crosshands/contract';
|
|
2
|
+
import type { StableAppIdentity } from '@crosshands/runtime';
|
|
3
|
+
type JsonRecord = Record<string, unknown>;
|
|
4
|
+
export type NativeFrame = {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
error?: string;
|
|
7
|
+
} & JsonRecord;
|
|
8
|
+
export type PowerShellLaunchSpec = {
|
|
9
|
+
executable: string;
|
|
10
|
+
args: string[];
|
|
11
|
+
env: NodeJS.ProcessEnv;
|
|
12
|
+
};
|
|
13
|
+
export declare function windowsPowerShellLaunchSpec(scriptPath: string, env?: NodeJS.ProcessEnv): PowerShellLaunchSpec;
|
|
14
|
+
export interface NativeWindowsTransport {
|
|
15
|
+
start(): Promise<NativeFrame>;
|
|
16
|
+
request(payload: JsonRecord, deadlineAt: number): Promise<NativeFrame>;
|
|
17
|
+
cancel(): Promise<void>;
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export declare class PowerShellStdioTransport implements NativeWindowsTransport {
|
|
21
|
+
#private;
|
|
22
|
+
constructor(scriptPath: string);
|
|
23
|
+
start(): Promise<NativeFrame>;
|
|
24
|
+
request(payload: JsonRecord, deadlineAt: number): Promise<NativeFrame>;
|
|
25
|
+
cancel(): Promise<void>;
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
type WindowsProviderOptions = {
|
|
29
|
+
scriptPath?: string;
|
|
30
|
+
manifestPath?: string;
|
|
31
|
+
transport?: NativeWindowsTransport;
|
|
32
|
+
graphicalSessionId?: string;
|
|
33
|
+
skipPayloadVerification?: boolean;
|
|
34
|
+
};
|
|
35
|
+
export declare function verifyWindowsPayload(scriptPath?: string, manifestPath?: string): Promise<void>;
|
|
36
|
+
export declare class WindowsComputerProvider implements ComputerProvider {
|
|
37
|
+
#private;
|
|
38
|
+
readonly generation: string;
|
|
39
|
+
constructor(options?: WindowsProviderOptions);
|
|
40
|
+
start(): Promise<ProviderHandshake>;
|
|
41
|
+
dispatch(request: ProviderRequest): Promise<ProviderResponse>;
|
|
42
|
+
inspect(operation: ComputerOperationName, input: unknown): Promise<{
|
|
43
|
+
bindings: ReferenceBindings;
|
|
44
|
+
appIdentity: StableAppIdentity;
|
|
45
|
+
} | null>;
|
|
46
|
+
cancel(_requestId: string): Promise<void>;
|
|
47
|
+
close(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
export declare function normalizeScreenshotIssues(value: unknown): JsonRecord[];
|
|
50
|
+
export {};
|
|
51
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAOA,OAAO,EAIL,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE5D,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAazC,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,UAAU,CAAA;AAEtE,MAAM,MAAM,oBAAoB,GAAG;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,GAAG,EAAE,MAAM,CAAC,UAAU,CAAA;CACvB,CAAA;AAED,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,MAAM,EAClB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,oBAAoB,CAmCtB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,IAAI,OAAO,CAAC,WAAW,CAAC,CAAA;IAC7B,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IACtE,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAED,qBAAa,wBAAyB,YAAW,sBAAsB;;gBAWzD,UAAU,EAAE,MAAM;IAIxB,KAAK,IAAI,OAAO,CAAC,WAAW,CAAC;IAoCnC,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAoChE,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAIvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAsD7B;AAED,KAAK,sBAAsB,GAAG;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,sBAAsB,CAAA;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAClC,CAAA;AAKD,wBAAsB,oBAAoB,CACxC,UAAU,SAAkB,EAC5B,YAAY,SAAoB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAqBf;AAsDD,qBAAa,uBAAwB,YAAW,gBAAgB;;IAC9D,QAAQ,CAAC,UAAU,SAA4B;gBAUnC,OAAO,GAAE,sBAA2B;IAa1C,KAAK,IAAI,OAAO,CAAC,iBAAiB,CAAC;IA2BnC,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAmC7D,OAAO,CACX,SAAS,EAAE,qBAAqB,EAChC,KAAK,EAAE,OAAO,GACb,OAAO,CAAC;QACT,QAAQ,EAAE,iBAAiB,CAAA;QAC3B,WAAW,EAAE,iBAAiB,CAAA;KAC/B,GAAG,IAAI,CAAC;IAgCH,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAyL7B;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,EAAE,CAMtE"}
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
import { isAbsolute, win32 } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { COMPUTER_OPERATIONS, CONTRACT_VERSIONS, createComputerError } from '@crosshands/contract';
|
|
8
|
+
export function windowsPowerShellLaunchSpec(scriptPath, env = process.env) {
|
|
9
|
+
const systemRoot = env.SystemRoot ?? env.SYSTEMROOT ?? 'C:\\Windows';
|
|
10
|
+
const executable = win32.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
11
|
+
const absoluteScript = win32.isAbsolute(scriptPath)
|
|
12
|
+
? win32.normalize(scriptPath)
|
|
13
|
+
: win32.resolve(scriptPath);
|
|
14
|
+
return {
|
|
15
|
+
executable,
|
|
16
|
+
args: [
|
|
17
|
+
'-NoLogo',
|
|
18
|
+
'-NoProfile',
|
|
19
|
+
'-NonInteractive',
|
|
20
|
+
'-ExecutionPolicy',
|
|
21
|
+
'Bypass',
|
|
22
|
+
'-File',
|
|
23
|
+
absoluteScript
|
|
24
|
+
],
|
|
25
|
+
env: {
|
|
26
|
+
SystemRoot: systemRoot,
|
|
27
|
+
WINDIR: env.WINDIR ?? systemRoot,
|
|
28
|
+
TEMP: env.TEMP ?? win32.join(systemRoot, 'Temp'),
|
|
29
|
+
TMP: env.TMP ?? env.TEMP ?? win32.join(systemRoot, 'Temp'),
|
|
30
|
+
USERPROFILE: env.USERPROFILE,
|
|
31
|
+
LOCALAPPDATA: env.LOCALAPPDATA,
|
|
32
|
+
APPDATA: env.APPDATA,
|
|
33
|
+
PSModuleAutoLoadingPreference: 'None',
|
|
34
|
+
POWERSHELL_TELEMETRY_OPTOUT: '1'
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export class PowerShellStdioTransport {
|
|
39
|
+
#launch;
|
|
40
|
+
#child;
|
|
41
|
+
#ready;
|
|
42
|
+
#resolveReady;
|
|
43
|
+
#rejectReady;
|
|
44
|
+
#active;
|
|
45
|
+
#tail = Promise.resolve();
|
|
46
|
+
constructor(scriptPath) {
|
|
47
|
+
this.#launch = windowsPowerShellLaunchSpec(scriptPath);
|
|
48
|
+
}
|
|
49
|
+
async start() {
|
|
50
|
+
if (this.#ready !== undefined)
|
|
51
|
+
return this.#ready;
|
|
52
|
+
this.#ready = new Promise((resolveReady, rejectReady) => {
|
|
53
|
+
this.#resolveReady = resolveReady;
|
|
54
|
+
this.#rejectReady = rejectReady;
|
|
55
|
+
});
|
|
56
|
+
const child = spawn(this.#launch.executable, this.#launch.args, {
|
|
57
|
+
env: this.#launch.env,
|
|
58
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
59
|
+
windowsHide: true,
|
|
60
|
+
shell: false
|
|
61
|
+
});
|
|
62
|
+
this.#child = child;
|
|
63
|
+
const lines = createInterface({ input: child.stdout, crlfDelay: Number.POSITIVE_INFINITY });
|
|
64
|
+
lines.on('line', (line) => this.#receive(line));
|
|
65
|
+
let stderr = '';
|
|
66
|
+
child.stderr.setEncoding('utf8');
|
|
67
|
+
child.stderr.on('data', (chunk) => {
|
|
68
|
+
if (stderr.length < 8_192)
|
|
69
|
+
stderr += chunk.slice(0, 8_192 - stderr.length);
|
|
70
|
+
});
|
|
71
|
+
child.once('error', (cause) => this.#fail(cause, child));
|
|
72
|
+
child.once('exit', (code) => this.#fail(createComputerError('provider_crashed', `Windows provider exited with code ${String(code)}`, {
|
|
73
|
+
stderr
|
|
74
|
+
}), child));
|
|
75
|
+
return this.#ready;
|
|
76
|
+
}
|
|
77
|
+
request(payload, deadlineAt) {
|
|
78
|
+
const result = this.#tail.then(async () => {
|
|
79
|
+
await this.start();
|
|
80
|
+
if (deadlineAt <= Date.now())
|
|
81
|
+
throw createComputerError('timeout', 'Request deadline elapsed');
|
|
82
|
+
const child = this.#child;
|
|
83
|
+
if (child === undefined || !child.stdin.writable)
|
|
84
|
+
throw createComputerError('provider_unavailable', 'Windows provider stdin is unavailable');
|
|
85
|
+
return new Promise((resolveFrame, rejectFrame) => {
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
this.#active = undefined;
|
|
88
|
+
if (this.#child === child) {
|
|
89
|
+
this.#child = undefined;
|
|
90
|
+
this.#ready = undefined;
|
|
91
|
+
this.#resolveReady = undefined;
|
|
92
|
+
this.#rejectReady = undefined;
|
|
93
|
+
}
|
|
94
|
+
child.kill();
|
|
95
|
+
rejectFrame(createComputerError('timeout', 'Windows provider request timed out'));
|
|
96
|
+
}, Math.max(1, deadlineAt - Date.now()));
|
|
97
|
+
timer.unref();
|
|
98
|
+
this.#active = { resolve: resolveFrame, reject: rejectFrame, timer };
|
|
99
|
+
child.stdin.write(`${JSON.stringify(payload)}\n`, 'utf8', (cause) => {
|
|
100
|
+
if (cause !== null && cause !== undefined)
|
|
101
|
+
this.#fail(cause, child);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
this.#tail = result.then(() => undefined, () => undefined);
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
async cancel() {
|
|
109
|
+
this.#child?.kill();
|
|
110
|
+
}
|
|
111
|
+
async close() {
|
|
112
|
+
const child = this.#child;
|
|
113
|
+
this.#child = undefined;
|
|
114
|
+
this.#ready = undefined;
|
|
115
|
+
this.#resolveReady = undefined;
|
|
116
|
+
this.#rejectReady = undefined;
|
|
117
|
+
child?.stdin.end();
|
|
118
|
+
child?.kill();
|
|
119
|
+
}
|
|
120
|
+
#receive(line) {
|
|
121
|
+
let frame;
|
|
122
|
+
try {
|
|
123
|
+
frame = JSON.parse(line);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
this.#fail(createComputerError('provider_crashed', 'Windows provider emitted non-JSON stdout'));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (this.#resolveReady !== undefined) {
|
|
130
|
+
const resolveReady = this.#resolveReady;
|
|
131
|
+
this.#resolveReady = undefined;
|
|
132
|
+
this.#rejectReady = undefined;
|
|
133
|
+
resolveReady(frame);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const active = this.#active;
|
|
137
|
+
if (active === undefined) {
|
|
138
|
+
this.#fail(createComputerError('provider_crashed', 'Windows provider emitted an unsolicited frame'));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.#active = undefined;
|
|
142
|
+
clearTimeout(active.timer);
|
|
143
|
+
active.resolve(frame);
|
|
144
|
+
}
|
|
145
|
+
#fail(cause, source) {
|
|
146
|
+
if (source !== undefined && this.#child !== source)
|
|
147
|
+
return;
|
|
148
|
+
this.#child = undefined;
|
|
149
|
+
this.#ready = undefined;
|
|
150
|
+
const rejectReady = this.#rejectReady;
|
|
151
|
+
this.#resolveReady = undefined;
|
|
152
|
+
this.#rejectReady = undefined;
|
|
153
|
+
rejectReady?.(cause);
|
|
154
|
+
const active = this.#active;
|
|
155
|
+
this.#active = undefined;
|
|
156
|
+
if (active !== undefined) {
|
|
157
|
+
clearTimeout(active.timer);
|
|
158
|
+
active.reject(cause);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const PACKAGED_SCRIPT = fileURLToPath(new URL('../assets/runtime.ps1', import.meta.url));
|
|
163
|
+
const PACKAGED_MANIFEST = fileURLToPath(new URL('../assets/payload.json', import.meta.url));
|
|
164
|
+
export async function verifyWindowsPayload(scriptPath = PACKAGED_SCRIPT, manifestPath = PACKAGED_MANIFEST) {
|
|
165
|
+
if (!isAbsolute(scriptPath) || !isAbsolute(manifestPath)) {
|
|
166
|
+
throw createComputerError('provider_unavailable', 'Windows payload paths must be absolute');
|
|
167
|
+
}
|
|
168
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
169
|
+
if (manifest.productVersion !== CONTRACT_VERSIONS.product) {
|
|
170
|
+
throw createComputerError('version_incompatible', 'Windows payload product version mismatch');
|
|
171
|
+
}
|
|
172
|
+
const expected = manifest.files?.['runtime.ps1'];
|
|
173
|
+
if (typeof expected !== 'string' || !/^[a-f0-9]{64}$/.test(expected)) {
|
|
174
|
+
throw createComputerError('provider_unavailable', 'Windows payload manifest is malformed');
|
|
175
|
+
}
|
|
176
|
+
const actual = createHash('sha256')
|
|
177
|
+
.update(await readFile(scriptPath))
|
|
178
|
+
.digest('hex');
|
|
179
|
+
if (actual !== expected) {
|
|
180
|
+
throw createComputerError('provider_unavailable', 'Windows provider payload hash mismatch');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function record(value) {
|
|
184
|
+
return value !== null && typeof value === 'object' ? value : {};
|
|
185
|
+
}
|
|
186
|
+
function errorCode(message) {
|
|
187
|
+
const normalized = message.toLowerCase();
|
|
188
|
+
if (normalized.includes('appblocked'))
|
|
189
|
+
return 'app_blocked';
|
|
190
|
+
if (normalized.includes('appnotfound'))
|
|
191
|
+
return 'app_not_found';
|
|
192
|
+
if (normalized.includes('windownotfound'))
|
|
193
|
+
return 'window_not_found';
|
|
194
|
+
if (normalized.includes('window_not_focused'))
|
|
195
|
+
return 'window_not_focused';
|
|
196
|
+
if (normalized.includes('stale_target') || normalized.includes('stale element'))
|
|
197
|
+
return 'stale_target';
|
|
198
|
+
if (normalized.includes('session_unavailable'))
|
|
199
|
+
return 'session_unavailable';
|
|
200
|
+
if (normalized.includes('unsupported_capability'))
|
|
201
|
+
return 'unsupported_capability';
|
|
202
|
+
if (normalized.includes('not settable'))
|
|
203
|
+
return 'value_not_settable';
|
|
204
|
+
if (normalized.includes('unknown element'))
|
|
205
|
+
return 'element_not_found';
|
|
206
|
+
if (normalized.includes('unsupported'))
|
|
207
|
+
return 'action_not_supported';
|
|
208
|
+
return 'accessibility_error';
|
|
209
|
+
}
|
|
210
|
+
function isPreDispatchFailure(message) {
|
|
211
|
+
return /^(app|window).*notfound|appblocked|session_unavailable|unsupported_capability|stale_target|window_not_focused/i.test(message);
|
|
212
|
+
}
|
|
213
|
+
function executableId(identity) {
|
|
214
|
+
const raw = `${identity.executablePath}|il:${identity.integrityRid}|pub:${identity.publisher}|sha256:${identity.sha256}`;
|
|
215
|
+
return raw.length <= 512 ? raw : `sha256:${createHash('sha256').update(raw).digest('hex')}`;
|
|
216
|
+
}
|
|
217
|
+
function targetReference(input) {
|
|
218
|
+
const candidates = [input.target, input.from, input.to];
|
|
219
|
+
for (const candidate of candidates) {
|
|
220
|
+
const candidateRecord = record(candidate);
|
|
221
|
+
if (candidateRecord.kind === 'element')
|
|
222
|
+
return record(candidateRecord.ref);
|
|
223
|
+
if ('contextToken' in candidateRecord)
|
|
224
|
+
return candidateRecord;
|
|
225
|
+
if (candidateRecord.kind === 'coordinate')
|
|
226
|
+
return record(candidateRecord.window);
|
|
227
|
+
}
|
|
228
|
+
return {};
|
|
229
|
+
}
|
|
230
|
+
function setBounded(map, key, value) {
|
|
231
|
+
map.delete(key);
|
|
232
|
+
map.set(key, value);
|
|
233
|
+
while (map.size > 32) {
|
|
234
|
+
const oldest = map.keys().next().value;
|
|
235
|
+
if (oldest === undefined)
|
|
236
|
+
return;
|
|
237
|
+
map.delete(oldest);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
export class WindowsComputerProvider {
|
|
241
|
+
generation = `windows-${randomUUID()}`;
|
|
242
|
+
#transport;
|
|
243
|
+
#graphicalSessionId;
|
|
244
|
+
#scriptPath;
|
|
245
|
+
#manifestPath;
|
|
246
|
+
#skipPayloadVerification;
|
|
247
|
+
#snapshots = new Map();
|
|
248
|
+
#identities = new Map();
|
|
249
|
+
#handshake;
|
|
250
|
+
constructor(options = {}) {
|
|
251
|
+
this.#scriptPath = options.scriptPath ?? PACKAGED_SCRIPT;
|
|
252
|
+
this.#manifestPath = options.manifestPath ?? PACKAGED_MANIFEST;
|
|
253
|
+
this.#skipPayloadVerification =
|
|
254
|
+
options.skipPayloadVerification ?? options.transport !== undefined;
|
|
255
|
+
this.#transport = options.transport ?? new PowerShellStdioTransport(this.#scriptPath);
|
|
256
|
+
this.#graphicalSessionId =
|
|
257
|
+
options.graphicalSessionId ??
|
|
258
|
+
process.env.CROSSHANDS_GRAPHICAL_SESSION_ID ??
|
|
259
|
+
process.env.SESSIONNAME ??
|
|
260
|
+
'win32:interactive';
|
|
261
|
+
}
|
|
262
|
+
async start() {
|
|
263
|
+
if (this.#handshake !== undefined)
|
|
264
|
+
return this.#handshake;
|
|
265
|
+
if (!this.#skipPayloadVerification) {
|
|
266
|
+
await verifyWindowsPayload(this.#scriptPath, this.#manifestPath);
|
|
267
|
+
}
|
|
268
|
+
const ready = await this.#transport.start();
|
|
269
|
+
if (!ready.ok || ready.ready !== true)
|
|
270
|
+
throw createComputerError('provider_unavailable', 'Windows provider readiness failed');
|
|
271
|
+
this.#handshake = {
|
|
272
|
+
provider: 'crosshands-platform-windows',
|
|
273
|
+
generation: this.generation,
|
|
274
|
+
graphicalSessionId: this.#graphicalSessionId,
|
|
275
|
+
providerProtocol: CONTRACT_VERSIONS.providerProtocol,
|
|
276
|
+
publicContract: CONTRACT_VERSIONS.publicContract,
|
|
277
|
+
capabilities: {
|
|
278
|
+
platform: 'win32',
|
|
279
|
+
provider: 'crosshands-platform-windows',
|
|
280
|
+
providerVersion: CONTRACT_VERSIONS.product,
|
|
281
|
+
operations: Object.fromEntries(Object.keys(COMPUTER_OPERATIONS).map((operation) => [operation, true])),
|
|
282
|
+
permissions: { accessibility: 'not_required', screenshots: 'not_required' }
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
return this.#handshake;
|
|
286
|
+
}
|
|
287
|
+
async dispatch(request) {
|
|
288
|
+
await this.start();
|
|
289
|
+
if (request.operation === 'capabilities')
|
|
290
|
+
return {
|
|
291
|
+
requestId: request.requestId,
|
|
292
|
+
dispatched: false,
|
|
293
|
+
result: this.#handshake.capabilities
|
|
294
|
+
};
|
|
295
|
+
if (request.operation === 'permissions')
|
|
296
|
+
return {
|
|
297
|
+
requestId: request.requestId,
|
|
298
|
+
dispatched: false,
|
|
299
|
+
result: { permissions: this.#handshake.capabilities.permissions }
|
|
300
|
+
};
|
|
301
|
+
const input = record(request.input);
|
|
302
|
+
const nativeInput = this.#nativeInput(request.operation, input);
|
|
303
|
+
const frame = await this.#transport.request(nativeInput, request.deadlineAt);
|
|
304
|
+
if (!frame.ok) {
|
|
305
|
+
const message = frame.error ?? 'Windows provider operation failed';
|
|
306
|
+
const dispatched = COMPUTER_OPERATIONS[request.operation].mutation
|
|
307
|
+
? !isPreDispatchFailure(message)
|
|
308
|
+
: false;
|
|
309
|
+
return {
|
|
310
|
+
requestId: request.requestId,
|
|
311
|
+
dispatched,
|
|
312
|
+
error: createComputerError(errorCode(message), message).toJSON()
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
requestId: request.requestId,
|
|
317
|
+
dispatched: COMPUTER_OPERATIONS[request.operation].mutation,
|
|
318
|
+
result: this.#normalizeResult(request.operation, frame)
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
async inspect(operation, input) {
|
|
322
|
+
if (!COMPUTER_OPERATIONS[operation].mutation)
|
|
323
|
+
return null;
|
|
324
|
+
const inputRecord = record(input);
|
|
325
|
+
const reference = targetReference(inputRecord);
|
|
326
|
+
const app = String(inputRecord.app ?? reference.appId ?? '');
|
|
327
|
+
if (app.length === 0)
|
|
328
|
+
return null;
|
|
329
|
+
await this.start();
|
|
330
|
+
const frame = await this.#transport.request({ tool: 'inspect_target', app }, Date.now() + 5_000);
|
|
331
|
+
if (!frame.ok)
|
|
332
|
+
throw createComputerError(errorCode(frame.error ?? ''), frame.error ?? 'Inspect failed');
|
|
333
|
+
const identity = frame.identity;
|
|
334
|
+
setBounded(this.#identities, app, identity);
|
|
335
|
+
const nativeExecutableId = executableId(identity);
|
|
336
|
+
return {
|
|
337
|
+
bindings: {
|
|
338
|
+
brokerGeneration: String(reference.brokerGeneration ?? 'unbound'),
|
|
339
|
+
providerGeneration: this.generation,
|
|
340
|
+
graphicalSessionId: this.#graphicalSessionId,
|
|
341
|
+
process: {
|
|
342
|
+
pid: identity.pid,
|
|
343
|
+
startedAt: identity.startedAt,
|
|
344
|
+
executableId: nativeExecutableId
|
|
345
|
+
},
|
|
346
|
+
appId: String(frame.appId),
|
|
347
|
+
window: { id: String(frame.windowId), ownerPid: identity.pid },
|
|
348
|
+
snapshotId: String(reference.snapshotId ?? 'inspection'),
|
|
349
|
+
desktopEpoch: Number(reference.desktopEpoch ?? 0)
|
|
350
|
+
},
|
|
351
|
+
appIdentity: { appId: String(frame.appId), executableId: nativeExecutableId }
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
async cancel(_requestId) {
|
|
355
|
+
await this.#transport.cancel();
|
|
356
|
+
}
|
|
357
|
+
async close() {
|
|
358
|
+
this.#handshake = undefined;
|
|
359
|
+
this.#snapshots.clear();
|
|
360
|
+
this.#identities.clear();
|
|
361
|
+
await this.#transport.close();
|
|
362
|
+
}
|
|
363
|
+
#nativeInput(operation, input) {
|
|
364
|
+
const toolNames = {
|
|
365
|
+
listApps: 'list_apps',
|
|
366
|
+
listWindows: 'list_windows',
|
|
367
|
+
getAppState: 'get_app_state',
|
|
368
|
+
performSecondaryAction: 'perform_secondary_action',
|
|
369
|
+
typeText: 'type_text',
|
|
370
|
+
pressKey: 'press_key',
|
|
371
|
+
pasteText: 'paste_text',
|
|
372
|
+
setValue: 'set_value'
|
|
373
|
+
};
|
|
374
|
+
const tool = toolNames[operation] ?? operation;
|
|
375
|
+
const reference = targetReference(input);
|
|
376
|
+
const app = String(input.app ?? reference.appId ?? '');
|
|
377
|
+
const cached = this.#snapshots.get(app);
|
|
378
|
+
const target = record(input.target);
|
|
379
|
+
const from = record(input.from);
|
|
380
|
+
const to = record(input.to);
|
|
381
|
+
const elementFor = (candidate) => {
|
|
382
|
+
if (candidate.kind !== 'element')
|
|
383
|
+
return undefined;
|
|
384
|
+
const ref = record(candidate.ref);
|
|
385
|
+
const index = Number(String(ref.ref ?? '').replace(/^element:/, ''));
|
|
386
|
+
const elements = Array.isArray(cached?.elements) ? cached.elements : [];
|
|
387
|
+
return Number.isInteger(index) ? elements[index] : undefined;
|
|
388
|
+
};
|
|
389
|
+
return {
|
|
390
|
+
tool,
|
|
391
|
+
app,
|
|
392
|
+
noScreenshot: input.captureScreenshot === false,
|
|
393
|
+
restoreWindow: input.restoreWindow === true,
|
|
394
|
+
expectedIdentity: this.#identities.get(app),
|
|
395
|
+
windowId: reference.window !== undefined ? record(reference.window).id : cached?.windowId,
|
|
396
|
+
windowBounds: cached?.windowBounds,
|
|
397
|
+
element: elementFor(target),
|
|
398
|
+
fromElement: elementFor(from),
|
|
399
|
+
toElement: elementFor(to),
|
|
400
|
+
x: target.x,
|
|
401
|
+
y: target.y,
|
|
402
|
+
from_x: from.x,
|
|
403
|
+
from_y: from.y,
|
|
404
|
+
to_x: to.x,
|
|
405
|
+
to_y: to.y,
|
|
406
|
+
duration_ms: input.durationMs,
|
|
407
|
+
click_count: input.clickCount,
|
|
408
|
+
mouse_button: input.button,
|
|
409
|
+
modifiers: input.modifiers,
|
|
410
|
+
action: input.action,
|
|
411
|
+
direction: input.direction,
|
|
412
|
+
pages: input.pages,
|
|
413
|
+
text: input.text,
|
|
414
|
+
key: Array.isArray(input.keys) ? input.keys.join('+') : input.key,
|
|
415
|
+
value: input.value,
|
|
416
|
+
...(operation === 'getAppState'
|
|
417
|
+
? {
|
|
418
|
+
windowId: record(input.window).id,
|
|
419
|
+
windowIndex: record(input.window).index
|
|
420
|
+
}
|
|
421
|
+
: {})
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
#normalizeResult(operation, frame) {
|
|
425
|
+
if (operation === 'listApps') {
|
|
426
|
+
return {
|
|
427
|
+
apps: (Array.isArray(frame.apps) ? frame.apps : []).map((value) => {
|
|
428
|
+
const app = record(value);
|
|
429
|
+
return {
|
|
430
|
+
id: String(app.bundleId),
|
|
431
|
+
name: String(app.name),
|
|
432
|
+
bundleId: String(app.bundleId),
|
|
433
|
+
pid: Number(app.pid),
|
|
434
|
+
isRunning: true
|
|
435
|
+
};
|
|
436
|
+
})
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
if (operation === 'listWindows') {
|
|
440
|
+
return {
|
|
441
|
+
windows: (Array.isArray(frame.windows) ? frame.windows : []).map((value) => {
|
|
442
|
+
const window = record(value);
|
|
443
|
+
const app = record(window.app);
|
|
444
|
+
return {
|
|
445
|
+
id: String(window.id),
|
|
446
|
+
appId: String(app.bundleId),
|
|
447
|
+
title: String(window.title ?? ''),
|
|
448
|
+
index: Number(window.index),
|
|
449
|
+
bounds: {
|
|
450
|
+
x: Number(window.x ?? 0),
|
|
451
|
+
y: Number(window.y ?? 0),
|
|
452
|
+
width: Math.max(1, Number(window.width ?? 1)),
|
|
453
|
+
height: Math.max(1, Number(window.height ?? 1))
|
|
454
|
+
},
|
|
455
|
+
minimized: Boolean(window.isMinimized)
|
|
456
|
+
};
|
|
457
|
+
})
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
if (operation === 'getAppState')
|
|
461
|
+
return this.#snapshotResult(record(frame.snapshot));
|
|
462
|
+
const action = record(frame.action);
|
|
463
|
+
const verification = record(action.verification);
|
|
464
|
+
return {
|
|
465
|
+
outcome: verification.state === 'verified'
|
|
466
|
+
? { state: 'verified', evidence: verification }
|
|
467
|
+
: {
|
|
468
|
+
state: 'indeterminate',
|
|
469
|
+
reason: String(verification.reason ?? 'native_action_unverified')
|
|
470
|
+
},
|
|
471
|
+
freshState: this.#snapshotResult(record(frame.snapshot))
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
#snapshotResult(native) {
|
|
475
|
+
const app = record(native.app);
|
|
476
|
+
const identity = native.processIdentity;
|
|
477
|
+
const bounds = record(native.windowBounds);
|
|
478
|
+
const appId = String(app.bundleId);
|
|
479
|
+
setBounded(this.#snapshots, appId, native);
|
|
480
|
+
setBounded(this.#identities, appId, identity);
|
|
481
|
+
const snapshot = {
|
|
482
|
+
id: String(native.snapshotId),
|
|
483
|
+
app: {
|
|
484
|
+
id: appId,
|
|
485
|
+
name: String(app.name),
|
|
486
|
+
bundleId: appId,
|
|
487
|
+
pid: Number(app.pid),
|
|
488
|
+
isRunning: true
|
|
489
|
+
},
|
|
490
|
+
window: {
|
|
491
|
+
id: String(native.windowId),
|
|
492
|
+
appId,
|
|
493
|
+
title: String(native.windowTitle ?? ''),
|
|
494
|
+
index: 0,
|
|
495
|
+
bounds: {
|
|
496
|
+
x: Number(bounds.x ?? 0),
|
|
497
|
+
y: Number(bounds.y ?? 0),
|
|
498
|
+
width: Math.max(1, Number(bounds.width ?? 1)),
|
|
499
|
+
height: Math.max(1, Number(bounds.height ?? 1))
|
|
500
|
+
},
|
|
501
|
+
minimized: false
|
|
502
|
+
},
|
|
503
|
+
treeText: (Array.isArray(native.treeLines) ? native.treeLines : []).join('\n'),
|
|
504
|
+
elementCount: Array.isArray(native.elements) ? native.elements.length : 0,
|
|
505
|
+
focusedElementRef: native.focusedElementId === null || native.focusedElementId === undefined
|
|
506
|
+
? null
|
|
507
|
+
: String(native.focusedElementId),
|
|
508
|
+
desktopEpoch: 0
|
|
509
|
+
};
|
|
510
|
+
return {
|
|
511
|
+
bindings: {
|
|
512
|
+
brokerGeneration: 'unbound',
|
|
513
|
+
providerGeneration: this.generation,
|
|
514
|
+
graphicalSessionId: this.#graphicalSessionId,
|
|
515
|
+
process: {
|
|
516
|
+
pid: identity.pid,
|
|
517
|
+
startedAt: identity.startedAt,
|
|
518
|
+
executableId: executableId(identity)
|
|
519
|
+
},
|
|
520
|
+
appId,
|
|
521
|
+
window: { id: String(native.windowId), ownerPid: identity.pid },
|
|
522
|
+
snapshotId: String(native.snapshotId),
|
|
523
|
+
desktopEpoch: 0
|
|
524
|
+
},
|
|
525
|
+
snapshot,
|
|
526
|
+
screenshot: typeof native.screenshotPngBase64 === 'string'
|
|
527
|
+
? {
|
|
528
|
+
format: 'png',
|
|
529
|
+
width: Number(native.screenshotWidth),
|
|
530
|
+
height: Number(native.screenshotHeight),
|
|
531
|
+
scale: Number(native.screenshotScale),
|
|
532
|
+
data: native.screenshotPngBase64
|
|
533
|
+
}
|
|
534
|
+
: null,
|
|
535
|
+
issues: normalizeScreenshotIssues(native.screenshotError)
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
export function normalizeScreenshotIssues(value) {
|
|
540
|
+
const error = record(value);
|
|
541
|
+
if (typeof error.message !== 'string' || error.message.length === 0)
|
|
542
|
+
return [];
|
|
543
|
+
return [
|
|
544
|
+
createComputerError('screenshot_failed', error.message, { component: 'screenshots' }).toJSON()
|
|
545
|
+
];
|
|
546
|
+
}
|
|
547
|
+
//# sourceMappingURL=provider.js.map
|