@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.
@@ -0,0 +1,562 @@
1
+ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
2
+ import { createHash } from 'node:crypto'
3
+ import { lstat, readFile } from 'node:fs/promises'
4
+ import { isAbsolute } from 'node:path'
5
+
6
+ import { negotiateVersionHandshake, type ContractVersions } from '@crosshands/contract'
7
+ import { DEFAULT_MAX_CONTROL_MESSAGE_BYTES, encodeFrame } from '@crosshands/runtime'
8
+
9
+ const RELAY_MAGIC = 0x53504858
10
+ const RELAY_VERSION = 1
11
+ const HEADER_BYTES = 20
12
+ const MAX_RELAY_PAYLOAD_BYTES = 4 * 1024 * 1024
13
+
14
+ export const WINDOWS_CONTROL_RELAY_RECORD = {
15
+ open: 1,
16
+ data: 2,
17
+ close: 3,
18
+ error: 4,
19
+ ready: 5,
20
+ shutdown: 6
21
+ } as const
22
+
23
+ export type WindowsControlPeer = {
24
+ pid: number
25
+ userSid: string
26
+ logonSid: string
27
+ logonSessionId: string
28
+ integrityRid: number
29
+ windowsSessionId: number
30
+ processStartedAt: string
31
+ executablePath: string
32
+ remote: false
33
+ }
34
+
35
+ export type WindowsControlIdentity = {
36
+ osIdentity: string
37
+ graphicalSessionId: string
38
+ }
39
+
40
+ export function windowsControlIdentity(peer: WindowsControlPeer): WindowsControlIdentity {
41
+ return {
42
+ osIdentity: `sid:${peer.userSid}`,
43
+ graphicalSessionId: `logon:${peer.logonSessionId}:session:${peer.windowsSessionId}`
44
+ }
45
+ }
46
+
47
+ export type WindowsControlRelayRecord = {
48
+ type: number
49
+ connectionId: bigint
50
+ payload: Buffer
51
+ }
52
+
53
+ export class WindowsControlRelayDecoder {
54
+ #buffer = Buffer.alloc(0)
55
+
56
+ push(chunk: Buffer): WindowsControlRelayRecord[] {
57
+ this.#buffer = Buffer.concat([this.#buffer, chunk])
58
+ const records: WindowsControlRelayRecord[] = []
59
+ while (this.#buffer.byteLength >= HEADER_BYTES) {
60
+ if (this.#buffer.readUInt32LE(0) !== RELAY_MAGIC) throw new Error('Invalid relay magic')
61
+ if (this.#buffer.readUInt16LE(4) !== RELAY_VERSION) throw new Error('Invalid relay version')
62
+ const length = this.#buffer.readUInt32LE(16)
63
+ if (length > MAX_RELAY_PAYLOAD_BYTES) throw new Error('Relay payload exceeds limit')
64
+ if (this.#buffer.byteLength < HEADER_BYTES + length) break
65
+ records.push({
66
+ type: this.#buffer.readUInt16LE(6),
67
+ connectionId: this.#buffer.readBigUInt64LE(8),
68
+ payload: this.#buffer.subarray(HEADER_BYTES, HEADER_BYTES + length)
69
+ })
70
+ this.#buffer = this.#buffer.subarray(HEADER_BYTES + length)
71
+ }
72
+ return records
73
+ }
74
+ }
75
+
76
+ export function encodeWindowsControlRelayRecord(
77
+ type: number,
78
+ connectionId: bigint,
79
+ payload: Buffer = Buffer.alloc(0)
80
+ ): Buffer {
81
+ if (payload.byteLength > MAX_RELAY_PAYLOAD_BYTES) throw new Error('Relay payload exceeds limit')
82
+ const frame = Buffer.allocUnsafe(HEADER_BYTES + payload.byteLength)
83
+ frame.writeUInt32LE(RELAY_MAGIC, 0)
84
+ frame.writeUInt16LE(RELAY_VERSION, 4)
85
+ frame.writeUInt16LE(type, 6)
86
+ frame.writeBigUInt64LE(connectionId, 8)
87
+ frame.writeUInt32LE(payload.byteLength, 16)
88
+ payload.copy(frame, HEADER_BYTES)
89
+ return frame
90
+ }
91
+
92
+ export type WindowsControlRelayOptions = {
93
+ helperPath: string
94
+ helperSha256: string
95
+ pipeName: string
96
+ onConnection: (connection: WindowsControlConnection) => void
97
+ spawnProcess?: typeof spawn
98
+ }
99
+
100
+ export type WindowsRelayControlRequest = {
101
+ requestId: string
102
+ payload: unknown
103
+ deadlineAt: number
104
+ peer: WindowsControlPeer
105
+ }
106
+
107
+ export type WindowsRelayControlServerOptions = {
108
+ relay: Omit<WindowsControlRelayOptions, 'onConnection'>
109
+ identity: WindowsControlIdentity
110
+ handler: (request: WindowsRelayControlRequest) => Promise<unknown>
111
+ createRelay?: (options: WindowsControlRelayOptions) => WindowsControlRelay
112
+ maxFrameBytes?: number
113
+ maxFramesPerConnection?: number
114
+ now?: () => number
115
+ }
116
+
117
+ export interface WindowsControlRelay {
118
+ start(): Promise<void>
119
+ close(): Promise<void>
120
+ }
121
+
122
+ export type WindowsControlConnection = {
123
+ id: bigint
124
+ peer: WindowsControlPeer
125
+ write(payload: Buffer): void
126
+ close(): void
127
+ onData(listener: (payload: Buffer) => void): void
128
+ onClose(listener: () => void): void
129
+ }
130
+
131
+ function parsePeer(payload: Buffer): WindowsControlPeer {
132
+ const raw = JSON.parse(payload.toString('utf8')) as Partial<WindowsControlPeer>
133
+ if (
134
+ !Number.isSafeInteger(raw.pid) ||
135
+ (raw.pid ?? 0) <= 0 ||
136
+ typeof raw.userSid !== 'string' ||
137
+ typeof raw.logonSid !== 'string' ||
138
+ typeof raw.logonSessionId !== 'string' ||
139
+ !Number.isSafeInteger(raw.integrityRid) ||
140
+ !Number.isSafeInteger(raw.windowsSessionId) ||
141
+ typeof raw.processStartedAt !== 'string' ||
142
+ typeof raw.executablePath !== 'string' ||
143
+ raw.remote !== false
144
+ ) {
145
+ throw new Error('Native relay returned incomplete peer identity')
146
+ }
147
+ return raw as WindowsControlPeer
148
+ }
149
+
150
+ export async function verifyWindowsControlRelay(
151
+ options: WindowsControlRelayOptions
152
+ ): Promise<void> {
153
+ if (!isAbsolute(options.helperPath)) throw new Error('Windows relay path must be absolute')
154
+ if (!/^\\\\\.\\pipe\\crosshands-[a-f0-9]{24}$/.test(options.pipeName)) {
155
+ throw new Error('Windows relay pipe name is not a CrossHands session endpoint')
156
+ }
157
+ if (!/^[a-f0-9]{64}$/.test(options.helperSha256)) {
158
+ throw new Error('Windows relay SHA-256 is malformed')
159
+ }
160
+ const info = await lstat(options.helperPath)
161
+ if (!info.isFile() || info.isSymbolicLink()) {
162
+ throw new Error('Windows relay payload is not a regular package file')
163
+ }
164
+ const bytes = await readFile(options.helperPath)
165
+ const actual = createHash('sha256').update(bytes).digest('hex')
166
+ if (actual !== options.helperSha256) throw new Error('Windows relay payload hash mismatch')
167
+ }
168
+
169
+ type ConnectionState = {
170
+ dataListeners: Array<(payload: Buffer) => void>
171
+ closeListeners: Array<() => void>
172
+ }
173
+
174
+ /**
175
+ * Launches the native process that owns the named pipe. Node never creates or
176
+ * accepts a Windows pipe directly: unverified bytes cannot reach the broker.
177
+ */
178
+ export class NativeWindowsControlRelay implements WindowsControlRelay {
179
+ readonly #options: WindowsControlRelayOptions
180
+ readonly #connections = new Map<bigint, ConnectionState>()
181
+ #child: ChildProcessWithoutNullStreams | undefined
182
+ #ready: Promise<void> | undefined
183
+
184
+ constructor(options: WindowsControlRelayOptions) {
185
+ this.#options = options
186
+ }
187
+
188
+ async start(): Promise<void> {
189
+ if (this.#child !== undefined) return this.#ready
190
+ if (process.platform !== 'win32') throw new Error('Windows relay can only run on Windows')
191
+ await verifyWindowsControlRelay(this.#options)
192
+ const child = (this.#options.spawnProcess ?? spawn)(
193
+ this.#options.helperPath,
194
+ ['--pipe', this.#options.pipeName, '--broker-pid', String(process.pid)],
195
+ {
196
+ cwd: undefined,
197
+ windowsHide: true,
198
+ stdio: ['pipe', 'pipe', 'pipe'],
199
+ env: {
200
+ SystemRoot: process.env.SystemRoot ?? process.env.SYSTEMROOT ?? 'C:\\Windows',
201
+ WINDIR: process.env.WINDIR ?? process.env.SystemRoot ?? 'C:\\Windows'
202
+ }
203
+ }
204
+ )
205
+ this.#child = child
206
+ const decoder = new WindowsControlRelayDecoder()
207
+ this.#ready = new Promise<void>((resolve, reject) => {
208
+ let settled = false
209
+ let stderrBytes = 0
210
+ const fail = (error: Error): void => {
211
+ if (!settled) {
212
+ settled = true
213
+ reject(error)
214
+ }
215
+ this.#failAll()
216
+ }
217
+ child.once('error', (cause) => fail(cause))
218
+ child.once('exit', (code) =>
219
+ fail(new Error(`Windows relay exited with code ${String(code)}`))
220
+ )
221
+ child.stderr.on('data', (chunk: Buffer) => {
222
+ stderrBytes += chunk.byteLength
223
+ if (stderrBytes > 64 * 1024) child.kill()
224
+ })
225
+ child.stdout.on('data', (chunk: Buffer) => {
226
+ try {
227
+ for (const record of decoder.push(chunk)) {
228
+ if (record.type === WINDOWS_CONTROL_RELAY_RECORD.ready) {
229
+ if (!settled) {
230
+ settled = true
231
+ resolve()
232
+ }
233
+ continue
234
+ }
235
+ this.#receive(record)
236
+ }
237
+ } catch (cause) {
238
+ child.kill()
239
+ fail(cause instanceof Error ? cause : new Error('Windows relay protocol failed'))
240
+ }
241
+ })
242
+ })
243
+ return this.#ready
244
+ }
245
+
246
+ #receive(record: WindowsControlRelayRecord): void {
247
+ if (record.type === WINDOWS_CONTROL_RELAY_RECORD.open) {
248
+ if (this.#connections.has(record.connectionId)) throw new Error('Duplicate relay connection')
249
+ const state: ConnectionState = { dataListeners: [], closeListeners: [] }
250
+ this.#connections.set(record.connectionId, state)
251
+ const connection: WindowsControlConnection = {
252
+ id: record.connectionId,
253
+ peer: parsePeer(record.payload),
254
+ write: (payload) =>
255
+ this.#write(WINDOWS_CONTROL_RELAY_RECORD.data, record.connectionId, payload),
256
+ close: () => this.#write(WINDOWS_CONTROL_RELAY_RECORD.close, record.connectionId),
257
+ onData: (listener) => state.dataListeners.push(listener),
258
+ onClose: (listener) => state.closeListeners.push(listener)
259
+ }
260
+ this.#options.onConnection(connection)
261
+ return
262
+ }
263
+ const state = this.#connections.get(record.connectionId)
264
+ if (state === undefined) throw new Error('Relay referenced an unknown connection')
265
+ if (record.type === WINDOWS_CONTROL_RELAY_RECORD.data) {
266
+ for (const listener of state.dataListeners) listener(record.payload)
267
+ return
268
+ }
269
+ if (
270
+ record.type === WINDOWS_CONTROL_RELAY_RECORD.close ||
271
+ record.type === WINDOWS_CONTROL_RELAY_RECORD.error
272
+ ) {
273
+ this.#connections.delete(record.connectionId)
274
+ for (const listener of state.closeListeners) listener()
275
+ return
276
+ }
277
+ throw new Error('Unknown relay record type')
278
+ }
279
+
280
+ #write(type: number, connectionId: bigint, payload?: Buffer): void {
281
+ const stdin = this.#child?.stdin
282
+ if (stdin === undefined || !stdin.writable) throw new Error('Windows relay is not writable')
283
+ if (!stdin.write(encodeWindowsControlRelayRecord(type, connectionId, payload))) {
284
+ this.#child?.kill()
285
+ throw new Error('Windows relay backpressure limit reached')
286
+ }
287
+ }
288
+
289
+ #failAll(): void {
290
+ for (const state of this.#connections.values()) {
291
+ for (const listener of state.closeListeners) listener()
292
+ }
293
+ this.#connections.clear()
294
+ }
295
+
296
+ async close(): Promise<void> {
297
+ const child = this.#child
298
+ this.#child = undefined
299
+ this.#ready = undefined
300
+ if (child === undefined) return
301
+ if (child.stdin.writable) {
302
+ child.stdin.end(encodeWindowsControlRelayRecord(WINDOWS_CONTROL_RELAY_RECORD.shutdown, 0n))
303
+ }
304
+ await new Promise<void>((resolve) => {
305
+ const timer = setTimeout(() => {
306
+ child.kill()
307
+ resolve()
308
+ }, 2_000)
309
+ timer.unref()
310
+ child.once('exit', () => {
311
+ clearTimeout(timer)
312
+ resolve()
313
+ })
314
+ })
315
+ this.#failAll()
316
+ }
317
+ }
318
+
319
+ type ControlMessage = Record<string, unknown> & { type: string; requestId?: string }
320
+
321
+ class ControlFrameDecoder {
322
+ readonly #maxBytes: number
323
+ #buffer = Buffer.alloc(0)
324
+
325
+ constructor(maxBytes: number) {
326
+ this.#maxBytes = maxBytes
327
+ }
328
+
329
+ push(chunk: Buffer): unknown[] {
330
+ this.#buffer = Buffer.concat([this.#buffer, chunk])
331
+ const frames: unknown[] = []
332
+ while (this.#buffer.byteLength >= 4) {
333
+ const length = this.#buffer.readUInt32BE(0)
334
+ if (length > this.#maxBytes) throw new Error('Control frame exceeds limit')
335
+ if (this.#buffer.byteLength < 4 + length) break
336
+ const payload = this.#buffer.subarray(4, 4 + length)
337
+ this.#buffer = this.#buffer.subarray(4 + length)
338
+ frames.push(JSON.parse(payload.toString('utf8')) as unknown)
339
+ }
340
+ return frames
341
+ }
342
+ }
343
+
344
+ function controlError(
345
+ requestId: string | undefined,
346
+ code: string,
347
+ message: string
348
+ ): Record<string, unknown> {
349
+ return { type: 'error', requestId: requestId ?? '', code, message }
350
+ }
351
+
352
+ function sameControlIdentity(left: unknown, right: WindowsControlIdentity): boolean {
353
+ if (left === null || typeof left !== 'object') return false
354
+ const identity = left as Record<string, unknown>
355
+ return (
356
+ identity.osIdentity === right.osIdentity &&
357
+ identity.graphicalSessionId === right.graphicalSessionId
358
+ )
359
+ }
360
+
361
+ /**
362
+ * Control-protocol server for Windows. The relay calls onConnection only after
363
+ * native DACL, locality, SID, logon-session, Windows-session, PID and integrity
364
+ * verification. This layer still requires a versioned handshake before it
365
+ * parses a request envelope or invokes the broker handler.
366
+ */
367
+ export class WindowsRelayControlServer {
368
+ readonly #options: WindowsRelayControlServerOptions
369
+ readonly #relay: WindowsControlRelay
370
+ readonly #maxFrameBytes: number
371
+ readonly #maxFrames: number
372
+ readonly #now: () => number
373
+ #requestCount = 0
374
+ #closed = false
375
+
376
+ constructor(options: WindowsRelayControlServerOptions) {
377
+ this.#options = options
378
+ this.#maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_CONTROL_MESSAGE_BYTES
379
+ this.#maxFrames = options.maxFramesPerConnection ?? 1024
380
+ this.#now = options.now ?? Date.now
381
+ this.#relay = (
382
+ options.createRelay ?? ((relayOptions) => new NativeWindowsControlRelay(relayOptions))
383
+ )({
384
+ ...options.relay,
385
+ onConnection: (connection) => this.#accept(connection)
386
+ })
387
+ }
388
+
389
+ get requestCount(): number {
390
+ return this.#requestCount
391
+ }
392
+
393
+ async start(): Promise<void> {
394
+ if (this.#closed) throw new Error('Windows control server is closed')
395
+ await this.#relay.start()
396
+ }
397
+
398
+ #accept(connection: WindowsControlConnection): void {
399
+ const decoder = new ControlFrameDecoder(this.#maxFrameBytes)
400
+ let authenticated = false
401
+ let frameCount = 0
402
+ let chain = Promise.resolve()
403
+ let closed = false
404
+
405
+ const close = (): void => {
406
+ if (closed) return
407
+ closed = true
408
+ try {
409
+ connection.close()
410
+ } catch {
411
+ // The relay already failed closed; there is no usable peer left to notify.
412
+ }
413
+ }
414
+ const send = (message: unknown): void => {
415
+ if (!closed) connection.write(encodeFrame(message, this.#maxFrameBytes))
416
+ }
417
+ const sendAndClose = (message: unknown): void => {
418
+ send(message)
419
+ close()
420
+ }
421
+
422
+ connection.onClose(() => {
423
+ closed = true
424
+ })
425
+ connection.onData((chunk) => {
426
+ chain = chain
427
+ .then(async () => {
428
+ for (const raw of decoder.push(chunk)) {
429
+ frameCount += 1
430
+ if (frameCount > this.#maxFrames) throw new Error('Control frame limit exceeded')
431
+ if (raw === null || typeof raw !== 'object' || !('type' in raw)) {
432
+ throw new Error('Malformed control message')
433
+ }
434
+ const message = raw as ControlMessage
435
+ if (!authenticated) {
436
+ if (message.type !== 'handshake') {
437
+ sendAndClose(
438
+ controlError(message.requestId, 'handshake_required', 'Handshake is required')
439
+ )
440
+ return
441
+ }
442
+ authenticated = this.#authenticate(message)
443
+ if (!authenticated) {
444
+ sendAndClose(
445
+ controlError(message.requestId, 'peer_rejected', 'Peer authentication failed')
446
+ )
447
+ return
448
+ }
449
+ const offeredVersions = (message as Record<string, unknown>).versions
450
+ if (offeredVersions === null || typeof offeredVersions !== 'object') {
451
+ authenticated = false
452
+ sendAndClose(
453
+ controlError(message.requestId, 'version_incompatible', 'Versions are required')
454
+ )
455
+ return
456
+ }
457
+ const negotiation = negotiateVersionHandshake(offeredVersions as ContractVersions)
458
+ if (!negotiation.ok) {
459
+ authenticated = false
460
+ sendAndClose(
461
+ controlError(message.requestId, negotiation.error.code, negotiation.error.message)
462
+ )
463
+ return
464
+ }
465
+ send({ type: 'handshake-ok', requestId: message.requestId })
466
+ continue
467
+ }
468
+ if (message.type !== 'request') {
469
+ sendAndClose(
470
+ controlError(message.requestId, 'invalid_message', 'Expected a request message')
471
+ )
472
+ return
473
+ }
474
+ // oxlint-disable-next-line no-await-in-loop -- one connection preserves frame order.
475
+ await this.#handleRequest(connection.peer, message, send)
476
+ }
477
+ })
478
+ .catch(() => close())
479
+ })
480
+ }
481
+
482
+ #authenticate(message: ControlMessage): boolean {
483
+ const record = message as Record<string, unknown>
484
+ return (
485
+ typeof message.requestId === 'string' &&
486
+ record.token === '' &&
487
+ sameControlIdentity(record.identity, this.#options.identity)
488
+ )
489
+ }
490
+
491
+ async #handleRequest(
492
+ peer: WindowsControlPeer,
493
+ message: ControlMessage,
494
+ send: (message: unknown) => void
495
+ ): Promise<void> {
496
+ if (
497
+ typeof message.requestId !== 'string' ||
498
+ typeof message.deadlineAt !== 'number' ||
499
+ !Number.isFinite(message.deadlineAt)
500
+ ) {
501
+ send(
502
+ controlError(message.requestId, 'invalid_request', 'Request ID and deadline are required')
503
+ )
504
+ return
505
+ }
506
+ if (message.deadlineAt <= this.#now()) {
507
+ send(controlError(message.requestId, 'timeout', 'Request deadline elapsed'))
508
+ return
509
+ }
510
+ const deadlineAt = message.deadlineAt
511
+ this.#requestCount += 1
512
+ let timer: NodeJS.Timeout | undefined
513
+ try {
514
+ const timeout = new Promise<never>((_, reject) => {
515
+ timer = setTimeout(
516
+ () =>
517
+ reject(
518
+ Object.assign(new Error('Control request deadline elapsed'), { code: 'timeout' })
519
+ ),
520
+ deadlineAt - this.#now()
521
+ )
522
+ timer.unref()
523
+ })
524
+ const result = await Promise.race([
525
+ this.#options.handler({
526
+ requestId: message.requestId,
527
+ payload: message.payload,
528
+ deadlineAt,
529
+ peer
530
+ }),
531
+ timeout
532
+ ])
533
+ send({ type: 'response', requestId: message.requestId, result })
534
+ } catch (cause) {
535
+ const code =
536
+ typeof cause === 'object' && cause !== null && 'code' in cause
537
+ ? String(cause.code)
538
+ : 'request_failed'
539
+ send(
540
+ controlError(
541
+ message.requestId,
542
+ code,
543
+ cause instanceof Error ? cause.message : 'Request failed'
544
+ )
545
+ )
546
+ } finally {
547
+ if (timer !== undefined) clearTimeout(timer)
548
+ }
549
+ }
550
+
551
+ async close(): Promise<void> {
552
+ if (this.#closed) return
553
+ this.#closed = true
554
+ await this.#relay.close()
555
+ }
556
+ }
557
+
558
+ export function createWindowsControlServer(
559
+ options: WindowsRelayControlServerOptions
560
+ ): WindowsRelayControlServer {
561
+ return new WindowsRelayControlServer(options)
562
+ }