@push.rocks/smartrust 2.1.0 → 2.3.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_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.rustbridge.d.ts +16 -0
- package/dist_ts/classes.rustbridge.js +251 -26
- package/dist_ts/classes.rustbridgerequesterror.d.ts +1 -1
- package/dist_ts/classes.rustbridgerequesterror.js +1 -1
- package/dist_ts/classes.stdiotransport.d.ts +14 -0
- package/dist_ts/classes.stdiotransport.js +152 -3
- package/dist_ts/interfaces/config.d.ts +19 -1
- package/dist_ts/interfaces/ipc.d.ts +14 -0
- package/dist_ts/interfaces/transport.d.ts +4 -0
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/package.json +4 -4
- package/readme.md +80 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.rustbridge.ts +287 -28
- package/ts/classes.rustbridgerequesterror.ts +2 -0
- package/ts/classes.stdiotransport.ts +152 -2
- package/ts/interfaces/config.ts +19 -1
- package/ts/interfaces/ipc.ts +16 -0
- package/ts/interfaces/transport.ts +6 -0
- package/ts/plugins.ts +2 -1
|
@@ -8,13 +8,18 @@ import type {
|
|
|
8
8
|
|
|
9
9
|
const DEFAULT_DISCONNECT_GRACE_PERIOD_MS = 5000;
|
|
10
10
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
11
|
+
const SENSITIVE_FRAME_HEADER_SIZE = 16;
|
|
12
|
+
const SENSITIVE_FRAME_MAGIC = 'SMRS';
|
|
11
13
|
|
|
12
14
|
export interface IStdioTransportOptions {
|
|
13
15
|
binaryPath: string;
|
|
14
16
|
cliArgs: string[];
|
|
15
17
|
env?: Record<string, string>;
|
|
16
18
|
inheritedFileDescriptors?: readonly number[];
|
|
19
|
+
sensitiveByteChannel?: { maxPayloadSize: number };
|
|
17
20
|
maxPayloadSize: number;
|
|
21
|
+
/** Default for every cleanup path, including automatic stdin failure. */
|
|
22
|
+
terminationGracePeriodMs?: number;
|
|
18
23
|
logger: IRustBridgeLogger;
|
|
19
24
|
}
|
|
20
25
|
|
|
@@ -32,6 +37,9 @@ interface IStdioTransportGeneration {
|
|
|
32
37
|
sigtermAttempted: boolean;
|
|
33
38
|
sigkillAttempted: boolean;
|
|
34
39
|
sigtermError: Error | null;
|
|
40
|
+
sensitiveWriter: plugins.stream.Writable | null;
|
|
41
|
+
sensitiveUsed: boolean;
|
|
42
|
+
sensitiveError: Error | null;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
/**
|
|
@@ -47,7 +55,21 @@ export class StdioTransport extends plugins.events.EventEmitter implements IRust
|
|
|
47
55
|
|
|
48
56
|
constructor(options: IStdioTransportOptions) {
|
|
49
57
|
super();
|
|
50
|
-
|
|
58
|
+
if (options.terminationGracePeriodMs !== undefined
|
|
59
|
+
&& (!Number.isSafeInteger(options.terminationGracePeriodMs) || options.terminationGracePeriodMs < 0)) {
|
|
60
|
+
throw new RangeError('StdioTransport terminationGracePeriodMs must be a non-negative safe integer');
|
|
61
|
+
}
|
|
62
|
+
if (options.sensitiveByteChannel
|
|
63
|
+
&& (!Number.isSafeInteger(options.sensitiveByteChannel.maxPayloadSize)
|
|
64
|
+
|| options.sensitiveByteChannel.maxPayloadSize <= 0)) {
|
|
65
|
+
throw new RangeError('StdioTransport sensitiveByteChannel maxPayloadSize must be a positive safe integer');
|
|
66
|
+
}
|
|
67
|
+
this.options = {
|
|
68
|
+
...options,
|
|
69
|
+
sensitiveByteChannel: options.sensitiveByteChannel
|
|
70
|
+
? { ...options.sensitiveByteChannel }
|
|
71
|
+
: undefined,
|
|
72
|
+
};
|
|
51
73
|
this.lineScanner = new LineScanner(options.maxPayloadSize, options.logger);
|
|
52
74
|
}
|
|
53
75
|
|
|
@@ -55,6 +77,12 @@ export class StdioTransport extends plugins.events.EventEmitter implements IRust
|
|
|
55
77
|
return this._connected;
|
|
56
78
|
}
|
|
57
79
|
|
|
80
|
+
public get sensitiveFileDescriptor(): number | null {
|
|
81
|
+
return this.options.sensitiveByteChannel
|
|
82
|
+
? 3 + (this.options.inheritedFileDescriptors?.length ?? 0)
|
|
83
|
+
: null;
|
|
84
|
+
}
|
|
85
|
+
|
|
58
86
|
/**
|
|
59
87
|
* Spawn the child process. Resolves when the process is running (not necessarily ready).
|
|
60
88
|
*/
|
|
@@ -76,6 +104,7 @@ export class StdioTransport extends plugins.events.EventEmitter implements IRust
|
|
|
76
104
|
'pipe',
|
|
77
105
|
'pipe',
|
|
78
106
|
...(this.options.inheritedFileDescriptors ?? []),
|
|
107
|
+
...(this.options.sensitiveByteChannel ? ['pipe' as const] : []),
|
|
79
108
|
];
|
|
80
109
|
childProcess = plugins.childProcess.spawn(
|
|
81
110
|
this.options.binaryPath,
|
|
@@ -90,6 +119,12 @@ export class StdioTransport extends plugins.events.EventEmitter implements IRust
|
|
|
90
119
|
const closePromise = new Promise<void>((resolve) => {
|
|
91
120
|
resolveClose = resolve;
|
|
92
121
|
});
|
|
122
|
+
const sensitiveEndpoint = this.options.sensitiveByteChannel
|
|
123
|
+
? childProcess.stdio[this.sensitiveFileDescriptor!]
|
|
124
|
+
: null;
|
|
125
|
+
const sensitiveWriter = sensitiveEndpoint instanceof plugins.stream.Writable
|
|
126
|
+
? sensitiveEndpoint
|
|
127
|
+
: null;
|
|
93
128
|
const generation: IStdioTransportGeneration = {
|
|
94
129
|
childProcess,
|
|
95
130
|
stderrRemainder: '',
|
|
@@ -104,6 +139,9 @@ export class StdioTransport extends plugins.events.EventEmitter implements IRust
|
|
|
104
139
|
sigtermAttempted: false,
|
|
105
140
|
sigkillAttempted: false,
|
|
106
141
|
sigtermError: null,
|
|
142
|
+
sensitiveWriter,
|
|
143
|
+
sensitiveUsed: false,
|
|
144
|
+
sensitiveError: null,
|
|
107
145
|
};
|
|
108
146
|
|
|
109
147
|
this.generation = generation;
|
|
@@ -145,6 +183,23 @@ export class StdioTransport extends plugins.events.EventEmitter implements IRust
|
|
|
145
183
|
});
|
|
146
184
|
});
|
|
147
185
|
|
|
186
|
+
generation.sensitiveWriter?.on('error', (error: Error) => {
|
|
187
|
+
if (generation.sensitiveError) return;
|
|
188
|
+
generation.sensitiveError = error;
|
|
189
|
+
if (this.generation !== generation || generation.closed) return;
|
|
190
|
+
this._connected = false;
|
|
191
|
+
queueMicrotask(() => {
|
|
192
|
+
if (this.generation !== generation || generation.closed) return;
|
|
193
|
+
void this.disconnectAndWait().catch((disconnectError: unknown) => {
|
|
194
|
+
this.options.logger.log('error', 'Failed to clean up child after sensitive pipe error: '
|
|
195
|
+
+ this.getErrorMessage(disconnectError));
|
|
196
|
+
});
|
|
197
|
+
if (this.listenerCount('error') > 0) this.emit('error', error);
|
|
198
|
+
else if (generation.disconnectPromise) this.options.logger.log('error', 'Sensitive pipe error during disconnect: ' + error.message);
|
|
199
|
+
else this.emit('error', error);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
148
203
|
// Handle stderr with cross-chunk buffering.
|
|
149
204
|
childProcess.stderr?.on('data', (data: Buffer) => {
|
|
150
205
|
if (this.generation !== generation || generation.closed) {
|
|
@@ -192,6 +247,7 @@ export class StdioTransport extends plugins.events.EventEmitter implements IRust
|
|
|
192
247
|
this.destroyOwnedStdioEndpoint(generation, 'stdin');
|
|
193
248
|
this.destroyOwnedStdioEndpoint(generation, 'stdout');
|
|
194
249
|
this.destroyOwnedStdioEndpoint(generation, 'stderr');
|
|
250
|
+
this.destroySensitiveEndpoint(generation);
|
|
195
251
|
});
|
|
196
252
|
});
|
|
197
253
|
|
|
@@ -279,6 +335,49 @@ export class StdioTransport extends plugins.events.EventEmitter implements IRust
|
|
|
279
335
|
});
|
|
280
336
|
}
|
|
281
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Write the single sensitive frame owned by this child generation. The frame
|
|
340
|
+
* header and request id are non-secret; `data` is written without converting
|
|
341
|
+
* it to a string or copying its backing ArrayBuffer.
|
|
342
|
+
*/
|
|
343
|
+
public async writeSensitiveFrame(
|
|
344
|
+
requestId: string,
|
|
345
|
+
data: Uint8Array<ArrayBuffer>,
|
|
346
|
+
): Promise<void> {
|
|
347
|
+
if (!(data instanceof Uint8Array) || !(data.buffer instanceof ArrayBuffer)) {
|
|
348
|
+
throw new TypeError('sensitive bytes must use an ArrayBuffer-backed Uint8Array');
|
|
349
|
+
}
|
|
350
|
+
const generation = this.generation;
|
|
351
|
+
if (!generation || generation.closed || generation.childProcess !== this.childProcess
|
|
352
|
+
|| !this._connected || !generation.sensitiveWriter || generation.sensitiveWriter.destroyed) {
|
|
353
|
+
throw new Error('sensitive byte pipe not available');
|
|
354
|
+
}
|
|
355
|
+
if (generation.sensitiveUsed) throw new Error('sensitive byte pipe was already used');
|
|
356
|
+
generation.sensitiveUsed = true;
|
|
357
|
+
const maxPayloadSize = this.options.sensitiveByteChannel?.maxPayloadSize;
|
|
358
|
+
if (maxPayloadSize === undefined || data.byteLength > maxPayloadSize) {
|
|
359
|
+
throw new Error('sensitive byte payload exceeds its configured limit');
|
|
360
|
+
}
|
|
361
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(requestId)) throw new Error('invalid sensitive request id');
|
|
362
|
+
|
|
363
|
+
const requestIdBytes = Buffer.from(requestId, 'ascii');
|
|
364
|
+
const header = Buffer.alloc(SENSITIVE_FRAME_HEADER_SIZE + requestIdBytes.byteLength);
|
|
365
|
+
header.write(SENSITIVE_FRAME_MAGIC, 0, 4, 'ascii');
|
|
366
|
+
header.writeUInt8(1, 4);
|
|
367
|
+
header.writeUInt8(0, 5);
|
|
368
|
+
header.writeUInt16BE(requestIdBytes.byteLength, 6);
|
|
369
|
+
header.writeBigUInt64BE(BigInt(data.byteLength), 8);
|
|
370
|
+
requestIdBytes.copy(header, SENSITIVE_FRAME_HEADER_SIZE);
|
|
371
|
+
const payload = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
372
|
+
try {
|
|
373
|
+
await this.writeSensitiveChunk(generation, header, false);
|
|
374
|
+
await this.writeSensitiveChunk(generation, payload, true);
|
|
375
|
+
} catch (error: unknown) {
|
|
376
|
+
this.destroySensitiveEndpoint(generation);
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
282
381
|
/**
|
|
283
382
|
* Kill the child process without waiting for confirmation.
|
|
284
383
|
*/
|
|
@@ -305,7 +404,7 @@ export class StdioTransport extends plugins.events.EventEmitter implements IRust
|
|
|
305
404
|
public disconnectAndWait(
|
|
306
405
|
options: IRustTransportDisconnectOptions = {},
|
|
307
406
|
): Promise<void> {
|
|
308
|
-
const gracePeriodMs = options.gracePeriodMs ?? DEFAULT_DISCONNECT_GRACE_PERIOD_MS;
|
|
407
|
+
const gracePeriodMs = options.gracePeriodMs ?? this.options.terminationGracePeriodMs ?? DEFAULT_DISCONNECT_GRACE_PERIOD_MS;
|
|
309
408
|
if (!Number.isSafeInteger(gracePeriodMs) || gracePeriodMs < 0) {
|
|
310
409
|
throw new RangeError(
|
|
311
410
|
'StdioTransport gracePeriodMs must be a non-negative safe integer',
|
|
@@ -332,6 +431,7 @@ export class StdioTransport extends plugins.events.EventEmitter implements IRust
|
|
|
332
431
|
generation.disconnectPromise = disconnectPromise;
|
|
333
432
|
generation.rejectDisconnect = rejectDisconnect;
|
|
334
433
|
this._connected = false;
|
|
434
|
+
this.destroySensitiveEndpoint(generation);
|
|
335
435
|
void disconnectPromise.then(
|
|
336
436
|
() => {
|
|
337
437
|
if (generation.disconnectPromise === disconnectPromise) {
|
|
@@ -358,6 +458,7 @@ export class StdioTransport extends plugins.events.EventEmitter implements IRust
|
|
|
358
458
|
this.destroyOwnedStdioEndpoint(generation, 'stdin');
|
|
359
459
|
this.destroyOwnedStdioEndpoint(generation, 'stdout');
|
|
360
460
|
this.destroyOwnedStdioEndpoint(generation, 'stderr');
|
|
461
|
+
this.destroySensitiveEndpoint(generation);
|
|
361
462
|
return disconnectPromise;
|
|
362
463
|
}
|
|
363
464
|
|
|
@@ -480,6 +581,55 @@ export class StdioTransport extends plugins.events.EventEmitter implements IRust
|
|
|
480
581
|
}
|
|
481
582
|
}
|
|
482
583
|
|
|
584
|
+
private writeSensitiveChunk(
|
|
585
|
+
generation: IStdioTransportGeneration,
|
|
586
|
+
data: Uint8Array,
|
|
587
|
+
end: boolean,
|
|
588
|
+
): Promise<void> {
|
|
589
|
+
return new Promise<void>((resolve, reject) => {
|
|
590
|
+
const writer = generation.sensitiveWriter;
|
|
591
|
+
if (!writer || writer.destroyed || generation.closed || this.generation !== generation) {
|
|
592
|
+
reject(new Error('sensitive byte pipe not available'));
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
let settled = false;
|
|
596
|
+
const cleanup = (): void => {
|
|
597
|
+
writer.off('error', onError);
|
|
598
|
+
writer.off('close', onClose);
|
|
599
|
+
};
|
|
600
|
+
const settle = (error?: Error | null): void => {
|
|
601
|
+
if (settled) return;
|
|
602
|
+
settled = true;
|
|
603
|
+
cleanup();
|
|
604
|
+
if (error) reject(error);
|
|
605
|
+
else resolve();
|
|
606
|
+
};
|
|
607
|
+
const onError = (error: Error): void => settle(error);
|
|
608
|
+
const onClose = (): void => settle(new Error('sensitive byte pipe closed before write completion'));
|
|
609
|
+
writer.once('error', onError);
|
|
610
|
+
writer.once('close', onClose);
|
|
611
|
+
try {
|
|
612
|
+
if (end) writer.end(data, () => settle());
|
|
613
|
+
else writer.write(data, error => settle(error));
|
|
614
|
+
} catch (error: unknown) {
|
|
615
|
+
settle(this.toError(error, 'Failed to write sensitive byte pipe'));
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private destroySensitiveEndpoint(generation: IStdioTransportGeneration): void {
|
|
621
|
+
const endpoint = generation.sensitiveWriter;
|
|
622
|
+
if (!endpoint || endpoint.destroyed) return;
|
|
623
|
+
try {
|
|
624
|
+
endpoint.destroy();
|
|
625
|
+
} catch (error: unknown) {
|
|
626
|
+
const endpointError = this.toError(error, 'Failed to close sensitive byte pipe');
|
|
627
|
+
generation.rejectDisconnect?.(endpointError);
|
|
628
|
+
this.options.logger.log('error', endpointError.message);
|
|
629
|
+
if (this.listenerCount('error') > 0) this.emit('error', endpointError);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
483
633
|
private toError(error: unknown, prefix: string): Error {
|
|
484
634
|
return new Error(prefix + ': ' + this.getErrorMessage(error));
|
|
485
635
|
}
|
package/ts/interfaces/config.ts
CHANGED
|
@@ -19,7 +19,8 @@ export interface IRustBridgeRequestOptions {
|
|
|
19
19
|
export interface IRustBridgeTerminateOptions {
|
|
20
20
|
/**
|
|
21
21
|
* Time to allow an owned stdio child to exit after SIGTERM before SIGKILL.
|
|
22
|
-
* Zero requests immediate SIGKILL. Defaults to
|
|
22
|
+
* Zero requests immediate SIGKILL. Defaults to the bridge's
|
|
23
|
+
* terminationGracePeriodMs, or 5000 milliseconds when not configured.
|
|
23
24
|
*/
|
|
24
25
|
gracePeriodMs?: number;
|
|
25
26
|
}
|
|
@@ -60,10 +61,27 @@ export interface IRustBridgeOptions extends IBinaryLocatorOptions {
|
|
|
60
61
|
* closes the caller's source descriptors.
|
|
61
62
|
*/
|
|
62
63
|
inheritedFileDescriptors?: readonly number[];
|
|
64
|
+
/**
|
|
65
|
+
* Opt in to one owned, one-shot binary pipe for a sensitive request in each
|
|
66
|
+
* spawned stdio generation. The pipe is appended after caller-owned inherited
|
|
67
|
+
* descriptors, so their child descriptor numbers remain unchanged.
|
|
68
|
+
*/
|
|
69
|
+
sensitiveByteChannel?: {
|
|
70
|
+
/** Maximum raw sensitive payload size accepted by the bridge. */
|
|
71
|
+
maxPayloadSize: number;
|
|
72
|
+
};
|
|
63
73
|
/** Timeout for individual requests in ms (default: 30000) */
|
|
64
74
|
requestTimeoutMs?: number;
|
|
65
75
|
/** Timeout for the ready event during spawn in ms (default: 10000) */
|
|
66
76
|
readyTimeoutMs?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Default stdio SIGTERM-to-SIGKILL grace period in milliseconds, including
|
|
79
|
+
* automatic startup and stdin-failure cleanup (default: 5000).
|
|
80
|
+
* Must be a non-negative safe integer. Explicit terminate() options override
|
|
81
|
+
* it only before cleanup starts; concurrent cleanup keeps its first choice.
|
|
82
|
+
* Socket termination never signals the remote daemon.
|
|
83
|
+
*/
|
|
84
|
+
terminationGracePeriodMs?: number;
|
|
67
85
|
/** Additional environment variables for the child process */
|
|
68
86
|
env?: Record<string, string>;
|
|
69
87
|
/** Name of the ready event emitted by the Rust binary (default: 'ready') */
|
package/ts/interfaces/ipc.ts
CHANGED
|
@@ -5,6 +5,20 @@ export interface IManagementRequest {
|
|
|
5
5
|
id: string;
|
|
6
6
|
method: string;
|
|
7
7
|
params: Record<string, any>;
|
|
8
|
+
/** Value-free metadata for an associated one-shot binary frame. */
|
|
9
|
+
sensitive?: IManagementSensitivePayload;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface IManagementSensitivePayload {
|
|
13
|
+
protocol: 'smartrust-sensitive-v1';
|
|
14
|
+
fileDescriptor: number;
|
|
15
|
+
byteLength: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface IManagementSensitiveAck {
|
|
19
|
+
protocol: 'smartrust-sensitive-v1';
|
|
20
|
+
consumed: true;
|
|
21
|
+
byteLength: number;
|
|
8
22
|
}
|
|
9
23
|
|
|
10
24
|
/**
|
|
@@ -16,6 +30,8 @@ export interface IManagementResponse {
|
|
|
16
30
|
result?: any;
|
|
17
31
|
error?: string;
|
|
18
32
|
errorCode?: string;
|
|
33
|
+
/** Required after the receiver consumed the exact associated binary frame. */
|
|
34
|
+
sensitiveAck?: IManagementSensitiveAck;
|
|
19
35
|
}
|
|
20
36
|
|
|
21
37
|
/**
|
|
@@ -35,6 +35,12 @@ export interface IRustTransport extends plugins.events.EventEmitter {
|
|
|
35
35
|
/** Write a string to the transport. Handles backpressure. */
|
|
36
36
|
write(data: string): Promise<void>;
|
|
37
37
|
|
|
38
|
+
/** Child descriptor for the optional one-shot sensitive byte pipe. */
|
|
39
|
+
readonly sensitiveFileDescriptor?: number | null;
|
|
40
|
+
|
|
41
|
+
/** Write and close one correlated binary frame. Available only in stdio mode. */
|
|
42
|
+
writeSensitiveFrame?(requestId: string, data: Uint8Array<ArrayBuffer>): Promise<void>;
|
|
43
|
+
|
|
38
44
|
/** Disconnect the transport. For stdio: kills the process. For socket: closes the connection. */
|
|
39
45
|
disconnect(): void;
|
|
40
46
|
|
package/ts/plugins.ts
CHANGED
|
@@ -5,8 +5,9 @@ import * as childProcess from 'child_process';
|
|
|
5
5
|
import * as events from 'events';
|
|
6
6
|
import * as url from 'url';
|
|
7
7
|
import * as net from 'net';
|
|
8
|
+
import * as stream from 'stream';
|
|
8
9
|
|
|
9
|
-
export { path, fs, childProcess, events, url, net };
|
|
10
|
+
export { path, fs, childProcess, events, url, net, stream };
|
|
10
11
|
|
|
11
12
|
// @push.rocks scope
|
|
12
13
|
import * as smartpath from '@push.rocks/smartpath';
|