@crosshands/runtime 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/dist/broker/broker.d.ts +53 -0
- package/dist/broker/broker.d.ts.map +1 -0
- package/dist/broker/broker.js +346 -0
- package/dist/broker/broker.js.map +1 -0
- package/dist/bundle-lifecycle.d.ts +75 -0
- package/dist/bundle-lifecycle.d.ts.map +1 -0
- package/dist/bundle-lifecycle.js +247 -0
- package/dist/bundle-lifecycle.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc/control-transport.d.ts +59 -0
- package/dist/ipc/control-transport.d.ts.map +1 -0
- package/dist/ipc/control-transport.js +389 -0
- package/dist/ipc/control-transport.js.map +1 -0
- package/dist/ipc/endpoint.d.ts +15 -0
- package/dist/ipc/endpoint.d.ts.map +1 -0
- package/dist/ipc/endpoint.js +16 -0
- package/dist/ipc/endpoint.js.map +1 -0
- package/dist/ipc/framing.d.ts +4 -0
- package/dist/ipc/framing.d.ts.map +1 -0
- package/dist/ipc/framing.js +32 -0
- package/dist/ipc/framing.js.map +1 -0
- package/dist/ipc/unix.d.ts +17 -0
- package/dist/ipc/unix.d.ts.map +1 -0
- package/dist/ipc/unix.js +106 -0
- package/dist/ipc/unix.js.map +1 -0
- package/dist/ipc/windows.d.ts +13 -0
- package/dist/ipc/windows.d.ts.map +1 -0
- package/dist/ipc/windows.js +10 -0
- package/dist/ipc/windows.js.map +1 -0
- package/dist/policy/policy.d.ts +23 -0
- package/dist/policy/policy.d.ts.map +1 -0
- package/dist/policy/policy.js +53 -0
- package/dist/policy/policy.js.map +1 -0
- package/dist/providers/supervisor.d.ts +17 -0
- package/dist/providers/supervisor.d.ts.map +1 -0
- package/dist/providers/supervisor.js +122 -0
- package/dist/providers/supervisor.js.map +1 -0
- package/package.json +37 -0
- package/src/broker/broker.ts +439 -0
- package/src/bundle-lifecycle.ts +385 -0
- package/src/index.ts +9 -0
- package/src/ipc/control-transport.ts +517 -0
- package/src/ipc/endpoint.ts +27 -0
- package/src/ipc/framing.ts +31 -0
- package/src/ipc/unix.ts +110 -0
- package/src/ipc/windows.ts +26 -0
- package/src/policy/policy.ts +73 -0
- package/src/providers/supervisor.ts +140 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
COMPUTER_OPERATIONS,
|
|
5
|
+
InteractionContextStore,
|
|
6
|
+
createComputerError,
|
|
7
|
+
negotiateVersionHandshake,
|
|
8
|
+
parseOperationOutput,
|
|
9
|
+
type ComputerOperationName,
|
|
10
|
+
type ContractVersions,
|
|
11
|
+
type InteractionContext,
|
|
12
|
+
type ReferenceBindings,
|
|
13
|
+
type TargetReference
|
|
14
|
+
} from '@crosshands/contract'
|
|
15
|
+
|
|
16
|
+
import { assertAppAllowed, type StableAppIdentity } from '../policy/policy.js'
|
|
17
|
+
import { ProviderSupervisor } from '../providers/supervisor.js'
|
|
18
|
+
|
|
19
|
+
export type BrokerPeer = {
|
|
20
|
+
osIdentity: string
|
|
21
|
+
graphicalSessionId: string
|
|
22
|
+
verified: boolean
|
|
23
|
+
local: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type TargetInspection = {
|
|
27
|
+
bindings: ReferenceBindings
|
|
28
|
+
appIdentity: StableAppIdentity
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type BrokerRequest = {
|
|
32
|
+
operation: ComputerOperationName
|
|
33
|
+
input: unknown
|
|
34
|
+
deadlineMs?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type BrokerResponse = {
|
|
38
|
+
requestId: string
|
|
39
|
+
result: unknown
|
|
40
|
+
desktopEpoch: number
|
|
41
|
+
providerGeneration: string
|
|
42
|
+
context?: InteractionContext
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type LocalBrokerOptions = {
|
|
46
|
+
identity: BrokerPeer
|
|
47
|
+
generation?: string
|
|
48
|
+
providerFactory: ConstructorParameters<typeof ProviderSupervisor>[0]['providerFactory']
|
|
49
|
+
inspectTarget?: (
|
|
50
|
+
operation: ComputerOperationName,
|
|
51
|
+
input: unknown
|
|
52
|
+
) => Promise<TargetInspection | null>
|
|
53
|
+
publish?: (response: BrokerResponse) => Promise<void>
|
|
54
|
+
contextTtlMs?: number
|
|
55
|
+
now?: () => number
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class SerialQueue {
|
|
59
|
+
#tail: Promise<void> = Promise.resolve()
|
|
60
|
+
|
|
61
|
+
async run<T>(work: () => Promise<T>): Promise<T> {
|
|
62
|
+
const previous = this.#tail
|
|
63
|
+
let release!: () => void
|
|
64
|
+
this.#tail = new Promise<void>((resolve) => {
|
|
65
|
+
release = resolve
|
|
66
|
+
})
|
|
67
|
+
await previous
|
|
68
|
+
try {
|
|
69
|
+
return await work()
|
|
70
|
+
} finally {
|
|
71
|
+
release()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isMutation(operation: ComputerOperationName): boolean {
|
|
77
|
+
return COMPUTER_OPERATIONS[operation].mutation
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function assertedInputApp(input: unknown): void {
|
|
81
|
+
if (input === null || typeof input !== 'object') return
|
|
82
|
+
const app = (input as { app?: unknown }).app
|
|
83
|
+
if (typeof app === 'string' && app.length > 0) {
|
|
84
|
+
assertAppAllowed({ appId: app, executableId: app })
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function targetReference(input: unknown): TargetReference | undefined {
|
|
89
|
+
if (input === null || typeof input !== 'object') return undefined
|
|
90
|
+
const record = input as Record<string, unknown>
|
|
91
|
+
const candidate = record.target ?? record.from
|
|
92
|
+
if (candidate === null || typeof candidate !== 'object') return undefined
|
|
93
|
+
const target = candidate as Record<string, unknown>
|
|
94
|
+
if (target.kind === 'element') return target.ref as TargetReference
|
|
95
|
+
if (target.kind === 'coordinate' && target.window !== undefined)
|
|
96
|
+
return target.window as TargetReference
|
|
97
|
+
if ('contextToken' in target) return target as TargetReference
|
|
98
|
+
return undefined
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function bindReference(
|
|
102
|
+
context: InteractionContext,
|
|
103
|
+
current: ReferenceBindings,
|
|
104
|
+
kind: TargetReference['kind'],
|
|
105
|
+
ref: string
|
|
106
|
+
): TargetReference {
|
|
107
|
+
return {
|
|
108
|
+
...current,
|
|
109
|
+
kind,
|
|
110
|
+
ref,
|
|
111
|
+
contextToken: context.token,
|
|
112
|
+
expiresAt: context.expiresAt
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeTarget(
|
|
117
|
+
candidate: unknown,
|
|
118
|
+
context: InteractionContext,
|
|
119
|
+
current: ReferenceBindings
|
|
120
|
+
): unknown {
|
|
121
|
+
if (candidate === null || typeof candidate !== 'object') return candidate
|
|
122
|
+
const target = candidate as Record<string, unknown>
|
|
123
|
+
if (target.kind === 'element' && typeof target.elementIndex === 'number') {
|
|
124
|
+
return {
|
|
125
|
+
kind: 'element',
|
|
126
|
+
ref: bindReference(context, current, 'element', `element:${target.elementIndex}`)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (target.kind === 'coordinate' && target.window === undefined) {
|
|
130
|
+
return {
|
|
131
|
+
...target,
|
|
132
|
+
window: bindReference(context, current, 'window', current.window.id)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (target.kind === 'context-window') {
|
|
136
|
+
return bindReference(context, current, 'window', current.window.id)
|
|
137
|
+
}
|
|
138
|
+
return candidate
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeMutationInput(
|
|
142
|
+
input: unknown,
|
|
143
|
+
context: InteractionContext,
|
|
144
|
+
current: ReferenceBindings
|
|
145
|
+
): unknown {
|
|
146
|
+
if (input === null || typeof input !== 'object') return input
|
|
147
|
+
const record = input as Record<string, unknown>
|
|
148
|
+
return {
|
|
149
|
+
...record,
|
|
150
|
+
...(record.target === undefined
|
|
151
|
+
? {}
|
|
152
|
+
: { target: normalizeTarget(record.target, context, current) }),
|
|
153
|
+
...(record.from === undefined ? {} : { from: normalizeTarget(record.from, context, current) }),
|
|
154
|
+
...(record.to === undefined ? {} : { to: normalizeTarget(record.to, context, current) })
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function mutationReferences(input: unknown): TargetReference[] {
|
|
159
|
+
if (input === null || typeof input !== 'object') return []
|
|
160
|
+
const record = input as Record<string, unknown>
|
|
161
|
+
return [record.target, record.from, record.to]
|
|
162
|
+
.map((candidate) => targetReference({ target: candidate }))
|
|
163
|
+
.filter((candidate): candidate is TargetReference => candidate !== undefined)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function contextToken(input: unknown): string | undefined {
|
|
167
|
+
if (input === null || typeof input !== 'object') return undefined
|
|
168
|
+
const token = (input as Record<string, unknown>).contextToken
|
|
169
|
+
return typeof token === 'string' ? token : undefined
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function computerError(cause: unknown): { code?: string; message: string; toJSON?: () => unknown } {
|
|
173
|
+
if (cause instanceof Error) return cause
|
|
174
|
+
return { message: 'Unknown provider failure' }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function assertFreshReference(
|
|
178
|
+
reference: TargetReference,
|
|
179
|
+
context: InteractionContext,
|
|
180
|
+
current: ReferenceBindings,
|
|
181
|
+
now: number
|
|
182
|
+
): void {
|
|
183
|
+
const mismatches = [
|
|
184
|
+
[reference.contextToken !== context.token, 'context token'],
|
|
185
|
+
[reference.brokerGeneration !== context.brokerGeneration, 'context broker generation'],
|
|
186
|
+
[reference.providerGeneration !== context.providerGeneration, 'context provider generation'],
|
|
187
|
+
[reference.graphicalSessionId !== context.graphicalSessionId, 'context session'],
|
|
188
|
+
[reference.brokerGeneration !== current.brokerGeneration, 'current broker generation'],
|
|
189
|
+
[reference.providerGeneration !== current.providerGeneration, 'current provider generation'],
|
|
190
|
+
[reference.graphicalSessionId !== current.graphicalSessionId, 'current session'],
|
|
191
|
+
[reference.process.pid !== context.process.pid, 'context pid'],
|
|
192
|
+
[reference.process.startedAt !== context.process.startedAt, 'context process start'],
|
|
193
|
+
[reference.process.executableId !== context.process.executableId, 'context executable'],
|
|
194
|
+
[reference.process.pid !== current.process.pid, 'current pid'],
|
|
195
|
+
[reference.process.startedAt !== current.process.startedAt, 'current process start'],
|
|
196
|
+
[reference.process.executableId !== current.process.executableId, 'current executable'],
|
|
197
|
+
[reference.appId !== context.appId, 'context app'],
|
|
198
|
+
[reference.appId !== current.appId, 'current app'],
|
|
199
|
+
[reference.window.id !== context.window.id, 'context window'],
|
|
200
|
+
[reference.window.ownerPid !== context.window.ownerPid, 'context window owner'],
|
|
201
|
+
[reference.window.id !== current.window.id, 'current window'],
|
|
202
|
+
[reference.window.ownerPid !== current.window.ownerPid, 'current window owner'],
|
|
203
|
+
[reference.snapshotId !== context.snapshotId, 'context snapshot'],
|
|
204
|
+
[reference.snapshotId !== current.snapshotId, 'current snapshot'],
|
|
205
|
+
[reference.desktopEpoch !== context.desktopEpoch, 'context desktop epoch'],
|
|
206
|
+
[reference.desktopEpoch !== current.desktopEpoch, 'current desktop epoch'],
|
|
207
|
+
[Date.parse(reference.expiresAt) < now, 'reference expiration'],
|
|
208
|
+
[Date.parse(context.expiresAt) < now, 'context expiration']
|
|
209
|
+
]
|
|
210
|
+
.filter(([failed]) => failed)
|
|
211
|
+
.map(([, label]) => label)
|
|
212
|
+
if (mismatches.length > 0) {
|
|
213
|
+
throw createComputerError('stale_target', 'Target reference is no longer fresh', { mismatches })
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export class BrokerClient {
|
|
218
|
+
constructor(private readonly broker: LocalBroker) {}
|
|
219
|
+
|
|
220
|
+
request(request: BrokerRequest): Promise<BrokerResponse> {
|
|
221
|
+
return this.broker.request(request)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export class LocalBroker {
|
|
226
|
+
readonly generation: string
|
|
227
|
+
readonly #identity: BrokerPeer
|
|
228
|
+
readonly #supervisor: ProviderSupervisor
|
|
229
|
+
readonly #contexts: InteractionContextStore
|
|
230
|
+
readonly #inspectTarget: LocalBrokerOptions['inspectTarget']
|
|
231
|
+
readonly #publish: LocalBrokerOptions['publish']
|
|
232
|
+
readonly #now: () => number
|
|
233
|
+
readonly #queue = new SerialQueue()
|
|
234
|
+
#desktopEpoch = 0
|
|
235
|
+
#requestSequence = 0
|
|
236
|
+
|
|
237
|
+
constructor(options: LocalBrokerOptions) {
|
|
238
|
+
this.generation = options.generation ?? `broker-${randomUUID()}`
|
|
239
|
+
this.#identity = options.identity
|
|
240
|
+
this.#inspectTarget = options.inspectTarget
|
|
241
|
+
this.#publish = options.publish
|
|
242
|
+
this.#now = options.now ?? Date.now
|
|
243
|
+
this.#contexts = new InteractionContextStore({
|
|
244
|
+
...(options.contextTtlMs === undefined ? {} : { ttlMs: options.contextTtlMs }),
|
|
245
|
+
now: this.#now
|
|
246
|
+
})
|
|
247
|
+
this.#supervisor = new ProviderSupervisor({
|
|
248
|
+
providerFactory: options.providerFactory,
|
|
249
|
+
graphicalSessionId: options.identity.graphicalSessionId
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
get desktopEpoch(): number {
|
|
254
|
+
return this.#desktopEpoch
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async connect(handshake: {
|
|
258
|
+
peer: BrokerPeer
|
|
259
|
+
versions: ContractVersions
|
|
260
|
+
}): Promise<BrokerClient> {
|
|
261
|
+
const { peer } = handshake
|
|
262
|
+
if (!peer.verified || !peer.local) {
|
|
263
|
+
throw createComputerError('session_unavailable', 'Broker peer could not be verified locally')
|
|
264
|
+
}
|
|
265
|
+
if (
|
|
266
|
+
peer.osIdentity !== this.#identity.osIdentity ||
|
|
267
|
+
peer.graphicalSessionId !== this.#identity.graphicalSessionId
|
|
268
|
+
) {
|
|
269
|
+
throw createComputerError(
|
|
270
|
+
'session_unavailable',
|
|
271
|
+
'Broker peer belongs to a different OS identity or graphical session'
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
const versions = negotiateVersionHandshake(handshake.versions)
|
|
275
|
+
if (!versions.ok) throw versions.error
|
|
276
|
+
await this.#supervisor.start()
|
|
277
|
+
return new BrokerClient(this)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
issueContext(bindings: ReferenceBindings): InteractionContext {
|
|
281
|
+
return this.#contexts.issue({
|
|
282
|
+
...bindings,
|
|
283
|
+
brokerGeneration: this.generation,
|
|
284
|
+
providerGeneration: this.#supervisor.generation ?? bindings.providerGeneration,
|
|
285
|
+
graphicalSessionId: this.#identity.graphicalSessionId,
|
|
286
|
+
desktopEpoch: this.#desktopEpoch
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
resolveContext(token: string): InteractionContext {
|
|
291
|
+
return this.#contexts.resolve(token)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
request(request: BrokerRequest): Promise<BrokerResponse> {
|
|
295
|
+
return this.#queue.run(() => this.#requestLocked(request))
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async #requestLocked(request: BrokerRequest): Promise<BrokerResponse> {
|
|
299
|
+
const requestId = `broker-${++this.#requestSequence}`
|
|
300
|
+
const deadlineAt = this.#now() + (request.deadlineMs ?? 30_000)
|
|
301
|
+
const mutation = isMutation(request.operation)
|
|
302
|
+
assertedInputApp(request.input)
|
|
303
|
+
let context: InteractionContext | undefined
|
|
304
|
+
let inspectionInput = request.input
|
|
305
|
+
if (mutation) {
|
|
306
|
+
const token = contextToken(request.input)
|
|
307
|
+
if (token === undefined) {
|
|
308
|
+
throw createComputerError(
|
|
309
|
+
'invalid_argument',
|
|
310
|
+
'Mutation requires a context token and target'
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
context = this.#contexts.resolve(token)
|
|
314
|
+
// Context-window and index shorthand do not carry an identity until the
|
|
315
|
+
// broker binds them. Inspect the bound form so native providers can
|
|
316
|
+
// re-resolve the exact process/window before dispatch.
|
|
317
|
+
inspectionInput = normalizeMutationInput(request.input, context, context)
|
|
318
|
+
}
|
|
319
|
+
const inspection = await this.#inspectTarget?.(request.operation, inspectionInput)
|
|
320
|
+
if (inspection !== undefined && inspection !== null) assertAppAllowed(inspection.appIdentity)
|
|
321
|
+
|
|
322
|
+
let providerInput = request.input
|
|
323
|
+
if (mutation) {
|
|
324
|
+
if (inspection === undefined || inspection === null) {
|
|
325
|
+
throw createComputerError('stale_target', 'Target identity could not be re-resolved')
|
|
326
|
+
}
|
|
327
|
+
providerInput = normalizeMutationInput(request.input, context!, inspection.bindings)
|
|
328
|
+
const references = mutationReferences(providerInput)
|
|
329
|
+
if (references.length === 0) {
|
|
330
|
+
throw createComputerError('invalid_argument', 'Mutation requires a target selector')
|
|
331
|
+
}
|
|
332
|
+
for (const reference of references) {
|
|
333
|
+
assertFreshReference(reference, context!, inspection.bindings, this.#now())
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const providerResponse = await this.#supervisor.dispatch({
|
|
339
|
+
requestId,
|
|
340
|
+
operation: request.operation,
|
|
341
|
+
input: providerInput,
|
|
342
|
+
deadlineAt
|
|
343
|
+
})
|
|
344
|
+
if (mutation) {
|
|
345
|
+
const dispatched = providerResponse.dispatched
|
|
346
|
+
if (dispatched) this.#desktopEpoch += 1
|
|
347
|
+
const result =
|
|
348
|
+
'error' in providerResponse
|
|
349
|
+
? {
|
|
350
|
+
outcome: dispatched
|
|
351
|
+
? { state: 'indeterminate', reason: providerResponse.error.message }
|
|
352
|
+
: { state: 'not_attempted', error: providerResponse.error }
|
|
353
|
+
}
|
|
354
|
+
: providerResponse.result
|
|
355
|
+
const response = {
|
|
356
|
+
requestId,
|
|
357
|
+
result: this.#publicResult(request.operation, result),
|
|
358
|
+
desktopEpoch: this.#desktopEpoch,
|
|
359
|
+
providerGeneration: this.#supervisor.generation ?? 'unknown'
|
|
360
|
+
}
|
|
361
|
+
await this.#publish?.(response)
|
|
362
|
+
return response
|
|
363
|
+
}
|
|
364
|
+
return this.#observationResponse(requestId, request.operation, providerResponse.result)
|
|
365
|
+
} catch (cause) {
|
|
366
|
+
const error = computerError(cause)
|
|
367
|
+
if (!mutation && error.code === 'provider_crashed') {
|
|
368
|
+
await this.#supervisor.restart()
|
|
369
|
+
this.#contexts.invalidateAll()
|
|
370
|
+
const retry = await this.#supervisor.dispatch({
|
|
371
|
+
requestId: `${requestId}-retry`,
|
|
372
|
+
operation: request.operation,
|
|
373
|
+
input: request.input,
|
|
374
|
+
deadlineAt
|
|
375
|
+
})
|
|
376
|
+
if ('error' in retry) throw Object.assign(new Error(retry.error.message), retry.error)
|
|
377
|
+
return this.#observationResponse(requestId, request.operation, retry.result)
|
|
378
|
+
}
|
|
379
|
+
if (mutation && (error.code === 'provider_crashed' || error.code === 'timeout')) {
|
|
380
|
+
this.#desktopEpoch += 1
|
|
381
|
+
this.#contexts.invalidateAll()
|
|
382
|
+
const response = {
|
|
383
|
+
requestId,
|
|
384
|
+
result: this.#publicResult(request.operation, {
|
|
385
|
+
outcome: { state: 'indeterminate', reason: error.message }
|
|
386
|
+
}),
|
|
387
|
+
desktopEpoch: this.#desktopEpoch,
|
|
388
|
+
providerGeneration: this.#supervisor.generation ?? 'unknown'
|
|
389
|
+
}
|
|
390
|
+
await this.#publish?.(response)
|
|
391
|
+
return response
|
|
392
|
+
}
|
|
393
|
+
throw cause
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
#observationResponse(
|
|
398
|
+
requestId: string,
|
|
399
|
+
operation: ComputerOperationName,
|
|
400
|
+
result: unknown
|
|
401
|
+
): BrokerResponse {
|
|
402
|
+
let context: InteractionContext | undefined
|
|
403
|
+
const publicResult = this.#publicResult(operation, result)
|
|
404
|
+
if (publicResult !== null && typeof publicResult === 'object' && 'context' in publicResult)
|
|
405
|
+
context = (publicResult as { context: InteractionContext }).context
|
|
406
|
+
return {
|
|
407
|
+
requestId,
|
|
408
|
+
result: publicResult,
|
|
409
|
+
desktopEpoch: this.#desktopEpoch,
|
|
410
|
+
providerGeneration: this.#supervisor.generation ?? 'unknown',
|
|
411
|
+
...(context === undefined ? {} : { context })
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
#publicResult(operation: ComputerOperationName, result: unknown): unknown {
|
|
416
|
+
if (result === null || typeof result !== 'object')
|
|
417
|
+
return parseOperationOutput(operation, result)
|
|
418
|
+
const value = result as Record<string, unknown>
|
|
419
|
+
let publicResult: Record<string, unknown> = value
|
|
420
|
+
if ('bindings' in value) {
|
|
421
|
+
const { bindings, ...snapshot } = value
|
|
422
|
+
publicResult = { ...snapshot, context: this.issueContext(bindings as ReferenceBindings) }
|
|
423
|
+
} else if (
|
|
424
|
+
value.freshState !== null &&
|
|
425
|
+
typeof value.freshState === 'object' &&
|
|
426
|
+
'bindings' in value.freshState
|
|
427
|
+
) {
|
|
428
|
+
const { bindings, ...freshState } = value.freshState as Record<string, unknown>
|
|
429
|
+
publicResult = {
|
|
430
|
+
...value,
|
|
431
|
+
freshState: {
|
|
432
|
+
...freshState,
|
|
433
|
+
context: this.issueContext(bindings as ReferenceBindings)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return parseOperationOutput(operation, publicResult)
|
|
438
|
+
}
|
|
439
|
+
}
|