@crosshands/runtime 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/broker/broker.d.ts +4 -0
- package/dist/broker/broker.d.ts.map +1 -1
- package/dist/broker/broker.js +194 -47
- package/dist/broker/broker.js.map +1 -1
- package/dist/diagnostics/paths.d.ts +5 -0
- package/dist/diagnostics/paths.d.ts.map +1 -0
- package/dist/diagnostics/paths.js +25 -0
- package/dist/diagnostics/paths.js.map +1 -0
- package/dist/diagnostics/record.d.ts +99 -0
- package/dist/diagnostics/record.d.ts.map +1 -0
- package/dist/diagnostics/record.js +154 -0
- package/dist/diagnostics/record.js.map +1 -0
- package/dist/diagnostics/writer.d.ts +27 -0
- package/dist/diagnostics/writer.d.ts.map +1 -0
- package/dist/diagnostics/writer.js +95 -0
- package/dist/diagnostics/writer.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/ipc/control-transport.d.ts +6 -0
- package/dist/ipc/control-transport.d.ts.map +1 -1
- package/dist/ipc/control-transport.js +13 -1
- package/dist/ipc/control-transport.js.map +1 -1
- package/dist/ipc/endpoint.d.ts +1 -0
- package/dist/ipc/endpoint.d.ts.map +1 -1
- package/dist/ipc/endpoint.js +3 -0
- package/dist/ipc/endpoint.js.map +1 -1
- package/dist/private-directory.d.ts +2 -0
- package/dist/private-directory.d.ts.map +1 -0
- package/dist/private-directory.js +15 -0
- package/dist/private-directory.js.map +1 -0
- package/package.json +2 -2
- package/src/broker/broker.ts +233 -50
- package/src/diagnostics/paths.ts +30 -0
- package/src/diagnostics/record.ts +262 -0
- package/src/diagnostics/writer.ts +112 -0
- package/src/index.ts +4 -0
- package/src/ipc/control-transport.ts +20 -1
- package/src/ipc/endpoint.ts +4 -0
- package/src/private-directory.ts +14 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COMPUTER_OPERATIONS,
|
|
3
|
+
ComputerError,
|
|
4
|
+
type ComputerOperationName,
|
|
5
|
+
type MutationResult,
|
|
6
|
+
type ReferenceBindings,
|
|
7
|
+
type SnapshotResult
|
|
8
|
+
} from '@crosshands/contract'
|
|
9
|
+
|
|
10
|
+
export const DIAGNOSTIC_RECORD_VERSION = 1 as const
|
|
11
|
+
|
|
12
|
+
const ERROR_DETAIL_KEYS = new Set([
|
|
13
|
+
'mismatches',
|
|
14
|
+
'cause',
|
|
15
|
+
'expectedProviderProtocol',
|
|
16
|
+
'receivedProviderProtocol',
|
|
17
|
+
'expectedPublicContract',
|
|
18
|
+
'receivedPublicContract'
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
export type DiagnosticPlatform = 'darwin' | 'win32' | 'linux'
|
|
22
|
+
|
|
23
|
+
export type DiagnosticError = {
|
|
24
|
+
code: string
|
|
25
|
+
message: string
|
|
26
|
+
retry: boolean
|
|
27
|
+
remediation: string
|
|
28
|
+
details?: Record<string, string | number | boolean | string[]>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type DiagnosticTarget = {
|
|
32
|
+
appId: string
|
|
33
|
+
pid?: number
|
|
34
|
+
windowId?: string
|
|
35
|
+
snapshotId?: string
|
|
36
|
+
kind?: string
|
|
37
|
+
ref?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type DiagnosticTimings = {
|
|
41
|
+
queue: number
|
|
42
|
+
inspect: number
|
|
43
|
+
dispatch: number
|
|
44
|
+
total: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type DiagnosticRequestResult =
|
|
48
|
+
| {
|
|
49
|
+
type: 'mutation'
|
|
50
|
+
dispatched: boolean
|
|
51
|
+
outcome: { state: string; reason?: string }
|
|
52
|
+
}
|
|
53
|
+
| {
|
|
54
|
+
type: 'observation'
|
|
55
|
+
snapshotId?: string
|
|
56
|
+
elementCount?: number
|
|
57
|
+
focusedElementRef?: string | null
|
|
58
|
+
screenshot: boolean
|
|
59
|
+
issues: Array<{ code: string; retry: boolean; remediation: string; message?: string }>
|
|
60
|
+
}
|
|
61
|
+
| {
|
|
62
|
+
type: 'lookup'
|
|
63
|
+
appCount?: number
|
|
64
|
+
windowCount?: number
|
|
65
|
+
permissions?: Record<string, string>
|
|
66
|
+
}
|
|
67
|
+
| ({ type: 'error' } & DiagnosticError)
|
|
68
|
+
|
|
69
|
+
export type DiagnosticEnvelope = {
|
|
70
|
+
v: typeof DIAGNOSTIC_RECORD_VERSION
|
|
71
|
+
ts: string
|
|
72
|
+
session: string
|
|
73
|
+
brokerGeneration: string
|
|
74
|
+
providerGeneration?: string
|
|
75
|
+
desktopEpoch: number
|
|
76
|
+
platform: DiagnosticPlatform
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type DiagnosticRecord =
|
|
80
|
+
| (DiagnosticEnvelope & { kind: 'broker.start' })
|
|
81
|
+
| (DiagnosticEnvelope & { kind: 'broker.stop'; reason: string })
|
|
82
|
+
| (DiagnosticEnvelope & { kind: 'helper.start' })
|
|
83
|
+
| (DiagnosticEnvelope & {
|
|
84
|
+
kind: 'helper.crash'
|
|
85
|
+
requestId?: string
|
|
86
|
+
operation?: string
|
|
87
|
+
error: DiagnosticError
|
|
88
|
+
})
|
|
89
|
+
| (DiagnosticEnvelope & { kind: 'helper.restart'; previousGeneration?: string })
|
|
90
|
+
| (DiagnosticEnvelope & {
|
|
91
|
+
kind: 'client.handshake'
|
|
92
|
+
accepted: boolean
|
|
93
|
+
requestId: string
|
|
94
|
+
code?: string
|
|
95
|
+
})
|
|
96
|
+
| (DiagnosticEnvelope & {
|
|
97
|
+
kind: 'request'
|
|
98
|
+
requestId: string
|
|
99
|
+
operation: string
|
|
100
|
+
mutation: boolean
|
|
101
|
+
ms: DiagnosticTimings
|
|
102
|
+
target?: DiagnosticTarget
|
|
103
|
+
result: DiagnosticRequestResult
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
export type DiagnosticsSink = {
|
|
107
|
+
emit(record: DiagnosticRecord): void
|
|
108
|
+
close(): Promise<void>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function diagnosticPlatform(
|
|
112
|
+
platform: NodeJS.Platform = process.platform
|
|
113
|
+
): DiagnosticPlatform {
|
|
114
|
+
if (platform === 'darwin' || platform === 'win32' || platform === 'linux') return platform
|
|
115
|
+
return 'linux'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function emitDiagnostic(sink: DiagnosticsSink | undefined, record: DiagnosticRecord): void {
|
|
119
|
+
try {
|
|
120
|
+
sink?.emit(record)
|
|
121
|
+
} catch {
|
|
122
|
+
// Diagnostics must not fail computer-use.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function diagnosticError(cause: unknown): DiagnosticError {
|
|
127
|
+
if (cause instanceof ComputerError) {
|
|
128
|
+
const json = cause.toJSON()
|
|
129
|
+
return {
|
|
130
|
+
code: json.code,
|
|
131
|
+
message: json.message,
|
|
132
|
+
retry: json.retry,
|
|
133
|
+
remediation: json.remediation,
|
|
134
|
+
...diagnosticErrorDetails(json.details)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
code: 'provider_unavailable',
|
|
139
|
+
message: cause instanceof Error ? cause.message : 'Request failed',
|
|
140
|
+
retry: true,
|
|
141
|
+
remediation: 'run_doctor'
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function diagnosticTarget(
|
|
146
|
+
input: unknown,
|
|
147
|
+
bindings: ReferenceBindings | undefined
|
|
148
|
+
): DiagnosticTarget | undefined {
|
|
149
|
+
const appId = bindings?.appId ?? inputAppId(input)
|
|
150
|
+
if (appId === undefined) return undefined
|
|
151
|
+
const selector = inputSelector(input)
|
|
152
|
+
return {
|
|
153
|
+
appId,
|
|
154
|
+
...(bindings?.process.pid === undefined ? {} : { pid: bindings.process.pid }),
|
|
155
|
+
...(bindings?.window.id === undefined ? {} : { windowId: bindings.window.id }),
|
|
156
|
+
...(bindings?.snapshotId === undefined ? {} : { snapshotId: bindings.snapshotId }),
|
|
157
|
+
...(selector.kind === undefined ? {} : { kind: selector.kind }),
|
|
158
|
+
...(selector.ref === undefined ? {} : { ref: selector.ref })
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function diagnosticRequestResult(
|
|
163
|
+
operation: ComputerOperationName,
|
|
164
|
+
result: unknown,
|
|
165
|
+
dispatched: boolean | undefined
|
|
166
|
+
): DiagnosticRequestResult {
|
|
167
|
+
if (COMPUTER_OPERATIONS[operation].mutation) {
|
|
168
|
+
const { outcome } = result as MutationResult
|
|
169
|
+
return {
|
|
170
|
+
type: 'mutation',
|
|
171
|
+
dispatched: dispatched ?? true,
|
|
172
|
+
outcome: {
|
|
173
|
+
state: outcome.state,
|
|
174
|
+
...('reason' in outcome && typeof outcome.reason === 'string'
|
|
175
|
+
? { reason: outcome.reason }
|
|
176
|
+
: {})
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (operation === 'getAppState') {
|
|
181
|
+
const observation = result as SnapshotResult
|
|
182
|
+
return {
|
|
183
|
+
type: 'observation',
|
|
184
|
+
snapshotId: observation.snapshot.id,
|
|
185
|
+
elementCount: observation.snapshot.elementCount,
|
|
186
|
+
focusedElementRef: observation.snapshot.focusedElementRef,
|
|
187
|
+
screenshot: observation.screenshot !== null,
|
|
188
|
+
issues: observation.issues.map((issue) => {
|
|
189
|
+
const recorded: {
|
|
190
|
+
code: string
|
|
191
|
+
retry: boolean
|
|
192
|
+
remediation: string
|
|
193
|
+
message?: string
|
|
194
|
+
} = {
|
|
195
|
+
code: issue.code,
|
|
196
|
+
retry: issue.retry,
|
|
197
|
+
remediation: issue.remediation
|
|
198
|
+
}
|
|
199
|
+
if (issue.message !== undefined) recorded.message = issue.message
|
|
200
|
+
return recorded
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (operation === 'listApps') {
|
|
205
|
+
return { type: 'lookup', appCount: (result as { apps: readonly unknown[] }).apps.length }
|
|
206
|
+
}
|
|
207
|
+
if (operation === 'listWindows') {
|
|
208
|
+
return {
|
|
209
|
+
type: 'lookup',
|
|
210
|
+
windowCount: (result as { windows: readonly unknown[] }).windows.length
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (operation === 'capabilities' || operation === 'permissions') {
|
|
214
|
+
const permissions = (result as { permissions: Record<string, string> }).permissions
|
|
215
|
+
return { type: 'lookup', permissions }
|
|
216
|
+
}
|
|
217
|
+
return { type: 'lookup' }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function diagnosticErrorDetails(
|
|
221
|
+
details: unknown
|
|
222
|
+
): { details: NonNullable<DiagnosticError['details']> } | Record<string, never> {
|
|
223
|
+
if (details === null || typeof details !== 'object' || Array.isArray(details)) return {}
|
|
224
|
+
const result: NonNullable<DiagnosticError['details']> = {}
|
|
225
|
+
for (const [key, value] of Object.entries(details as Record<string, unknown>)) {
|
|
226
|
+
if (!ERROR_DETAIL_KEYS.has(key)) continue
|
|
227
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
228
|
+
result[key] = value
|
|
229
|
+
} else if (Array.isArray(value) && value.every((entry) => typeof entry === 'string')) {
|
|
230
|
+
result[key] = value
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return Object.keys(result).length > 0 ? { details: result } : {}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function inputAppId(input: unknown): string | undefined {
|
|
237
|
+
if (input === null || typeof input !== 'object') return undefined
|
|
238
|
+
const app = (input as { app?: unknown }).app
|
|
239
|
+
return typeof app === 'string' && app.length > 0 ? app : undefined
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function inputSelector(input: unknown): { kind?: string; ref?: string } {
|
|
243
|
+
if (input === null || typeof input !== 'object') return {}
|
|
244
|
+
const record = input as { target?: unknown; from?: unknown }
|
|
245
|
+
const selector = record.target ?? record.from
|
|
246
|
+
if (selector === null || typeof selector !== 'object') return {}
|
|
247
|
+
const target = selector as { kind?: unknown; elementIndex?: unknown; ref?: unknown }
|
|
248
|
+
let kind = typeof target.kind === 'string' ? target.kind : undefined
|
|
249
|
+
let ref: string | undefined
|
|
250
|
+
if (typeof target.elementIndex === 'number') ref = `element:${target.elementIndex}`
|
|
251
|
+
if (typeof target.ref === 'string') {
|
|
252
|
+
ref = target.ref
|
|
253
|
+
} else if (target.ref !== null && typeof target.ref === 'object') {
|
|
254
|
+
const bound = target.ref as { kind?: unknown; ref?: unknown }
|
|
255
|
+
if (typeof bound.kind === 'string') kind = bound.kind
|
|
256
|
+
if (typeof bound.ref === 'string') ref = bound.ref
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
...(kind === undefined ? {} : { kind }),
|
|
260
|
+
...(ref === undefined ? {} : { ref })
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { closeSync, fstatSync, fsyncSync, openSync, writeSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { ensurePrivateDirectory } from '../private-directory.js'
|
|
5
|
+
import type { DiagnosticRecord, DiagnosticsSink } from './record.js'
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_DIAGNOSTICS_ROTATE_BYTES = 5 * 1024 * 1024
|
|
8
|
+
|
|
9
|
+
export type JsonlFileWriterOptions = {
|
|
10
|
+
directory: string
|
|
11
|
+
generation: string
|
|
12
|
+
maxBytes?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class JsonlFileWriter {
|
|
16
|
+
readonly directory: string
|
|
17
|
+
readonly generation: string
|
|
18
|
+
readonly #maxBytes: number
|
|
19
|
+
#part = 1
|
|
20
|
+
#started = false
|
|
21
|
+
#closed = false
|
|
22
|
+
#fd: number | undefined
|
|
23
|
+
#bytes = 0
|
|
24
|
+
|
|
25
|
+
constructor(options: JsonlFileWriterOptions) {
|
|
26
|
+
this.directory = options.directory
|
|
27
|
+
this.generation = options.generation
|
|
28
|
+
this.#maxBytes = options.maxBytes ?? DEFAULT_DIAGNOSTICS_ROTATE_BYTES
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
start(): void {
|
|
32
|
+
if (this.#started) return
|
|
33
|
+
ensurePrivateDirectory(this.directory)
|
|
34
|
+
this.#started = true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
emit(record: Record<string, unknown>): void {
|
|
38
|
+
if (this.#closed) return
|
|
39
|
+
if (!this.#started) this.start()
|
|
40
|
+
const line = `${JSON.stringify(record)}\n`
|
|
41
|
+
const size = Buffer.byteLength(line)
|
|
42
|
+
if (this.#bytes > 0 && this.#bytes + size > this.#maxBytes) this.#rotate()
|
|
43
|
+
if (this.#fd === undefined) this.#open()
|
|
44
|
+
const fd = this.#fd
|
|
45
|
+
if (fd === undefined) return
|
|
46
|
+
writeSync(fd, line)
|
|
47
|
+
this.#bytes += size
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async close(): Promise<void> {
|
|
51
|
+
if (this.#closed) return
|
|
52
|
+
this.#closed = true
|
|
53
|
+
this.#closeFile()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#filePath(): string {
|
|
57
|
+
const suffix = this.#part === 1 ? '' : `.${this.#part}`
|
|
58
|
+
return join(this.directory, `${this.generation}${suffix}.jsonl`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#open(): void {
|
|
62
|
+
const fd = openSync(this.#filePath(), 'a', 0o600)
|
|
63
|
+
this.#fd = fd
|
|
64
|
+
this.#bytes = fstatSync(fd).size
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#closeFile(): void {
|
|
68
|
+
const fd = this.#fd
|
|
69
|
+
this.#fd = undefined
|
|
70
|
+
if (fd === undefined) return
|
|
71
|
+
try {
|
|
72
|
+
fsyncSync(fd)
|
|
73
|
+
} finally {
|
|
74
|
+
closeSync(fd)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#rotate(): void {
|
|
79
|
+
this.#closeFile()
|
|
80
|
+
this.#part += 1
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type JsonlDiagnosticsWriterOptions = JsonlFileWriterOptions
|
|
85
|
+
|
|
86
|
+
export class JsonlDiagnosticsWriter implements DiagnosticsSink {
|
|
87
|
+
readonly #file: JsonlFileWriter
|
|
88
|
+
|
|
89
|
+
constructor(options: JsonlDiagnosticsWriterOptions) {
|
|
90
|
+
this.#file = new JsonlFileWriter(options)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get directory(): string {
|
|
94
|
+
return this.#file.directory
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get generation(): string {
|
|
98
|
+
return this.#file.generation
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
start(): void {
|
|
102
|
+
this.#file.start()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
emit(record: DiagnosticRecord): void {
|
|
106
|
+
this.#file.emit(record)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
close(): Promise<void> {
|
|
110
|
+
return this.#file.close()
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export * from './broker/broker.js'
|
|
2
2
|
export * from './bundle-lifecycle.js'
|
|
3
|
+
export * from './diagnostics/paths.js'
|
|
4
|
+
export * from './diagnostics/record.js'
|
|
5
|
+
export * from './diagnostics/writer.js'
|
|
3
6
|
export * from './ipc/endpoint.js'
|
|
7
|
+
export * from './private-directory.js'
|
|
4
8
|
export * from './ipc/control-transport.js'
|
|
5
9
|
export * from './ipc/framing.js'
|
|
6
10
|
export * from './ipc/unix.js'
|
|
@@ -42,6 +42,12 @@ export type LocalControlRequest = {
|
|
|
42
42
|
deadlineAt: number
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export type ControlHandshakeEvent = {
|
|
46
|
+
accepted: boolean
|
|
47
|
+
requestId: string
|
|
48
|
+
code?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
export type LocalControlServerOptions = {
|
|
46
52
|
endpoint: BrokerEndpoint
|
|
47
53
|
runtimeDirectory?: string
|
|
@@ -53,6 +59,7 @@ export type LocalControlServerOptions = {
|
|
|
53
59
|
maxFrameBytes?: number
|
|
54
60
|
maxFramesPerConnection?: number
|
|
55
61
|
now?: () => number
|
|
62
|
+
onHandshake?: (event: ControlHandshakeEvent) => void
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
type ControlMessage = Record<string, unknown> & { type: string; requestId?: string }
|
|
@@ -254,6 +261,13 @@ export class LocalControlServer {
|
|
|
254
261
|
|
|
255
262
|
async #authenticate(message: ControlMessage, socket: Socket): Promise<boolean> {
|
|
256
263
|
const handshake = message as unknown as LocalControlHandshake
|
|
264
|
+
const note = (accepted: boolean, code?: string): void => {
|
|
265
|
+
this.#options.onHandshake?.({
|
|
266
|
+
accepted,
|
|
267
|
+
requestId: typeof handshake.requestId === 'string' ? handshake.requestId : '',
|
|
268
|
+
...(code === undefined ? {} : { code })
|
|
269
|
+
})
|
|
270
|
+
}
|
|
257
271
|
if (
|
|
258
272
|
typeof handshake.requestId !== 'string' ||
|
|
259
273
|
typeof handshake.token !== 'string' ||
|
|
@@ -265,6 +279,7 @@ export class LocalControlServer {
|
|
|
265
279
|
socket,
|
|
266
280
|
errorMessage(handshake.requestId, 'peer_rejected', 'Peer identity was rejected')
|
|
267
281
|
)
|
|
282
|
+
note(false, 'peer_rejected')
|
|
268
283
|
return false
|
|
269
284
|
}
|
|
270
285
|
const versions = negotiateVersionHandshake(handshake.versions)
|
|
@@ -278,6 +293,7 @@ export class LocalControlServer {
|
|
|
278
293
|
versions.error
|
|
279
294
|
)
|
|
280
295
|
)
|
|
296
|
+
note(false, versions.error.code)
|
|
281
297
|
return false
|
|
282
298
|
}
|
|
283
299
|
let accepted: boolean
|
|
@@ -297,8 +313,11 @@ export class LocalControlServer {
|
|
|
297
313
|
socket,
|
|
298
314
|
errorMessage(handshake.requestId, 'peer_rejected', 'Peer authentication failed')
|
|
299
315
|
)
|
|
316
|
+
note(false, 'peer_rejected')
|
|
317
|
+
return false
|
|
300
318
|
}
|
|
301
|
-
|
|
319
|
+
note(true)
|
|
320
|
+
return true
|
|
302
321
|
}
|
|
303
322
|
|
|
304
323
|
async #handleRequest(socket: Socket, message: ControlMessage): Promise<void> {
|
package/src/ipc/endpoint.ts
CHANGED
|
@@ -12,6 +12,10 @@ export type BrokerEndpointOptions = {
|
|
|
12
12
|
runtimeDirectory?: string
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export function graphicalSessionKey(graphicalSessionId: string): string {
|
|
16
|
+
return createHash('sha256').update(graphicalSessionId).digest('hex').slice(0, 12)
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
export function brokerEndpoint(options: BrokerEndpointOptions): BrokerEndpoint {
|
|
16
20
|
const key = createHash('sha256')
|
|
17
21
|
.update(`${options.osIdentity}\0${options.graphicalSessionId}`)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { chmodSync, lstatSync, mkdirSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
export function ensurePrivateDirectory(directory: string): void {
|
|
4
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 })
|
|
5
|
+
const info = lstatSync(directory)
|
|
6
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
7
|
+
throw new Error('CrossHands directory is unsafe')
|
|
8
|
+
}
|
|
9
|
+
const uid = process.getuid?.()
|
|
10
|
+
if (uid !== undefined && info.uid !== uid) {
|
|
11
|
+
throw new Error('CrossHands directory has another owner')
|
|
12
|
+
}
|
|
13
|
+
if (process.platform !== 'win32') chmodSync(directory, 0o700)
|
|
14
|
+
}
|