release configs

This commit is contained in:
EthanShoeDev
2025-10-06 23:31:28 -04:00
parent bfcd043743
commit 18a6e0be4f
19 changed files with 592 additions and 493 deletions

View File

@@ -20,21 +20,21 @@ import * as GeneratedRussh from './index';
// ─────────────────────────────────────────────────────────────────────────────
export type TerminalType =
| 'Vanilla'
| 'Vt100'
| 'Vt102'
| 'Vt220'
| 'Ansi'
| 'Xterm'
| 'Xterm256';
| 'Vanilla'
| 'Vt100'
| 'Vt102'
| 'Vt220'
| 'Ansi'
| 'Xterm'
| 'Xterm256';
export type ConnectionDetails = {
host: string;
port: number;
username: string;
security:
| { type: 'password'; password: string }
| { type: 'key'; privateKey: string };
host: string;
port: number;
username: string;
security:
| { type: 'password'; password: string }
| { type: 'key'; privateKey: string };
};
/**
@@ -44,58 +44,58 @@ export type ConnectionDetails = {
* It is no longer relevant after the connect() promise is resolved.
*/
export type SshConnectionProgress =
| 'tcpConnected' // TCP established, starting SSH handshake
| 'sshHandshake'; // SSH protocol negotiation complete
| 'tcpConnected' // TCP established, starting SSH handshake
| 'sshHandshake'; // SSH protocol negotiation complete
export type ConnectOptions = ConnectionDetails & {
onConnectionProgress?: (status: SshConnectionProgress) => void;
onDisconnected?: (connectionId: string) => void;
onServerKey: (
serverKeyInfo: GeneratedRussh.ServerPublicKeyInfo,
signal?: AbortSignal
) => Promise<boolean>;
abortSignal?: AbortSignal;
onConnectionProgress?: (status: SshConnectionProgress) => void;
onDisconnected?: (connectionId: string) => void;
onServerKey: (
serverKeyInfo: GeneratedRussh.ServerPublicKeyInfo,
signal?: AbortSignal,
) => Promise<boolean>;
abortSignal?: AbortSignal;
};
export type StartShellOptions = {
term: TerminalType;
terminalMode?: GeneratedRussh.TerminalMode[];
terminalPixelSize?: GeneratedRussh.TerminalPixelSize;
terminalSize?: GeneratedRussh.TerminalSize;
onClosed?: (shellId: number) => void;
abortSignal?: AbortSignal;
term: TerminalType;
terminalMode?: GeneratedRussh.TerminalMode[];
terminalPixelSize?: GeneratedRussh.TerminalPixelSize;
terminalSize?: GeneratedRussh.TerminalSize;
onClosed?: (shellId: number) => void;
abortSignal?: AbortSignal;
};
export type StreamKind = 'stdout' | 'stderr';
export type TerminalChunk = {
seq: bigint;
/** Milliseconds since UNIX epoch (double). */
tMs: number;
stream: StreamKind;
bytes: ArrayBuffer;
seq: bigint;
/** Milliseconds since UNIX epoch (double). */
tMs: number;
stream: StreamKind;
bytes: ArrayBuffer;
};
export type DropNotice = { kind: 'dropped'; fromSeq: bigint; toSeq: bigint };
export type ListenerEvent = TerminalChunk | DropNotice;
export type Cursor =
| { mode: 'head' } // earliest available in ring
| { mode: 'tailBytes'; bytes: bigint } // last N bytes (best-effort)
| { mode: 'seq'; seq: bigint } // from a given sequence
| { mode: 'time'; tMs: number } // from timestamp
| { mode: 'live' }; // no replay, live only
| { mode: 'head' } // earliest available in ring
| { mode: 'tailBytes'; bytes: bigint } // last N bytes (best-effort)
| { mode: 'seq'; seq: bigint } // from a given sequence
| { mode: 'time'; tMs: number } // from timestamp
| { mode: 'live' }; // no replay, live only
export type ListenerOptions = {
cursor: Cursor;
/** Optional per-listener coalescing window in ms (e.g., 1025). */
coalesceMs?: number;
cursor: Cursor;
/** Optional per-listener coalescing window in ms (e.g., 1025). */
coalesceMs?: number;
};
export type BufferReadResult = {
chunks: TerminalChunk[];
nextSeq: bigint;
dropped?: { fromSeq: bigint; toSeq: bigint };
chunks: TerminalChunk[];
nextSeq: bigint;
dropped?: { fromSeq: bigint; toSeq: bigint };
};
// ─────────────────────────────────────────────────────────────────────────────
@@ -103,66 +103,66 @@ export type BufferReadResult = {
// ─────────────────────────────────────────────────────────────────────────────
type ProgressTimings = {
tcpEstablishedAtMs: number;
sshHandshakeAtMs: number;
tcpEstablishedAtMs: number;
sshHandshakeAtMs: number;
};
export type SshConnection = {
readonly connectionId: string;
readonly createdAtMs: number;
readonly connectedAtMs: number;
readonly connectionDetails: ConnectionDetails;
readonly progressTimings: ProgressTimings;
readonly connectionId: string;
readonly createdAtMs: number;
readonly connectedAtMs: number;
readonly connectionDetails: ConnectionDetails;
readonly progressTimings: ProgressTimings;
startShell: (opts: StartShellOptions) => Promise<SshShell>;
disconnect: (opts?: { signal?: AbortSignal }) => Promise<void>;
startShell: (opts: StartShellOptions) => Promise<SshShell>;
disconnect: (opts?: { signal?: AbortSignal }) => Promise<void>;
};
export type SshShell = {
readonly channelId: number;
readonly createdAtMs: number;
readonly pty: TerminalType;
readonly connectionId: string;
readonly channelId: number;
readonly createdAtMs: number;
readonly pty: TerminalType;
readonly connectionId: string;
// I/O
sendData: (
data: ArrayBuffer,
opts?: { signal?: AbortSignal }
) => Promise<void>;
close: (opts?: { signal?: AbortSignal }) => Promise<void>;
// I/O
sendData: (
data: ArrayBuffer,
opts?: { signal?: AbortSignal },
) => Promise<void>;
close: (opts?: { signal?: AbortSignal }) => Promise<void>;
// Buffer policy & stats
// setBufferPolicy: (policy: {
// ringBytes?: number;
// coalesceMs?: number;
// }) => Promise<void>;
bufferStats: () => GeneratedRussh.BufferStats;
currentSeq: () => number;
// Buffer policy & stats
// setBufferPolicy: (policy: {
// ringBytes?: number;
// coalesceMs?: number;
// }) => Promise<void>;
bufferStats: () => GeneratedRussh.BufferStats;
currentSeq: () => number;
// Replay + live
readBuffer: (cursor: Cursor, maxBytes?: bigint) => BufferReadResult;
addListener: (
cb: (ev: ListenerEvent) => void,
opts: ListenerOptions
) => bigint;
removeListener: (id: bigint) => void;
// Replay + live
readBuffer: (cursor: Cursor, maxBytes?: bigint) => BufferReadResult;
addListener: (
cb: (ev: ListenerEvent) => void,
opts: ListenerOptions,
) => bigint;
removeListener: (id: bigint) => void;
};
type RusshApi = {
uniffiInitAsync: () => Promise<void>;
connect: (opts: ConnectOptions) => Promise<SshConnection>;
generateKeyPair: (
type: 'rsa' | 'ecdsa' | 'ed25519'
// TODO: Add these
// passphrase?: string;
// keySize?: number;
// comment?: string;
) => Promise<string>;
validatePrivateKey: (
key: string
) =>
| { valid: true; error?: never }
| { valid: false; error: GeneratedRussh.SshError };
uniffiInitAsync: () => Promise<void>;
connect: (opts: ConnectOptions) => Promise<SshConnection>;
generateKeyPair: (
type: 'rsa' | 'ecdsa' | 'ed25519',
// TODO: Add these
// passphrase?: string;
// keySize?: number;
// comment?: string;
) => Promise<string>;
validatePrivateKey: (
key: string,
) =>
| { valid: true; error?: never }
| { valid: false; error: GeneratedRussh.SshError };
};
// #endregion
@@ -170,242 +170,242 @@ type RusshApi = {
// #region Wrapper to match the ideal API
const terminalTypeLiteralToEnum = {
Vanilla: GeneratedRussh.TerminalType.Vanilla,
Vt100: GeneratedRussh.TerminalType.Vt100,
Vt102: GeneratedRussh.TerminalType.Vt102,
Vt220: GeneratedRussh.TerminalType.Vt220,
Ansi: GeneratedRussh.TerminalType.Ansi,
Xterm: GeneratedRussh.TerminalType.Xterm,
Xterm256: GeneratedRussh.TerminalType.Xterm256,
Vanilla: GeneratedRussh.TerminalType.Vanilla,
Vt100: GeneratedRussh.TerminalType.Vt100,
Vt102: GeneratedRussh.TerminalType.Vt102,
Vt220: GeneratedRussh.TerminalType.Vt220,
Ansi: GeneratedRussh.TerminalType.Ansi,
Xterm: GeneratedRussh.TerminalType.Xterm,
Xterm256: GeneratedRussh.TerminalType.Xterm256,
} as const satisfies Record<string, GeneratedRussh.TerminalType>;
const terminalTypeEnumToLiteral: Record<
GeneratedRussh.TerminalType,
TerminalType
GeneratedRussh.TerminalType,
TerminalType
> = {
[GeneratedRussh.TerminalType.Vanilla]: 'Vanilla',
[GeneratedRussh.TerminalType.Vt100]: 'Vt100',
[GeneratedRussh.TerminalType.Vt102]: 'Vt102',
[GeneratedRussh.TerminalType.Vt220]: 'Vt220',
[GeneratedRussh.TerminalType.Ansi]: 'Ansi',
[GeneratedRussh.TerminalType.Xterm]: 'Xterm',
[GeneratedRussh.TerminalType.Xterm256]: 'Xterm256',
[GeneratedRussh.TerminalType.Vanilla]: 'Vanilla',
[GeneratedRussh.TerminalType.Vt100]: 'Vt100',
[GeneratedRussh.TerminalType.Vt102]: 'Vt102',
[GeneratedRussh.TerminalType.Vt220]: 'Vt220',
[GeneratedRussh.TerminalType.Ansi]: 'Ansi',
[GeneratedRussh.TerminalType.Xterm]: 'Xterm',
[GeneratedRussh.TerminalType.Xterm256]: 'Xterm256',
};
const sshConnProgressEnumToLiteral = {
[GeneratedRussh.SshConnectionProgressEvent.TcpConnected]: 'tcpConnected',
[GeneratedRussh.SshConnectionProgressEvent.SshHandshake]: 'sshHandshake',
[GeneratedRussh.SshConnectionProgressEvent.TcpConnected]: 'tcpConnected',
[GeneratedRussh.SshConnectionProgressEvent.SshHandshake]: 'sshHandshake',
} as const satisfies Record<
GeneratedRussh.SshConnectionProgressEvent,
SshConnectionProgress
GeneratedRussh.SshConnectionProgressEvent,
SshConnectionProgress
>;
const streamEnumToLiteral = {
[GeneratedRussh.StreamKind.Stdout]: 'stdout',
[GeneratedRussh.StreamKind.Stderr]: 'stderr',
[GeneratedRussh.StreamKind.Stdout]: 'stdout',
[GeneratedRussh.StreamKind.Stderr]: 'stderr',
} as const satisfies Record<GeneratedRussh.StreamKind, StreamKind>;
function generatedConnDetailsToIdeal(
details: GeneratedRussh.ConnectionDetails
details: GeneratedRussh.ConnectionDetails,
): ConnectionDetails {
const security: ConnectionDetails['security'] =
details.security instanceof GeneratedRussh.Security.Password
? { type: 'password', password: details.security.inner.password }
: { type: 'key', privateKey: details.security.inner.privateKeyContent };
return {
host: details.host,
port: details.port,
username: details.username,
security,
};
const security: ConnectionDetails['security'] =
details.security instanceof GeneratedRussh.Security.Password
? { type: 'password', password: details.security.inner.password }
: { type: 'key', privateKey: details.security.inner.privateKeyContent };
return {
host: details.host,
port: details.port,
username: details.username,
security,
};
}
function cursorToGenerated(cursor: Cursor): GeneratedRussh.Cursor {
switch (cursor.mode) {
case 'head':
return new GeneratedRussh.Cursor.Head();
case 'tailBytes':
return new GeneratedRussh.Cursor.TailBytes({
bytes: cursor.bytes,
});
case 'seq':
return new GeneratedRussh.Cursor.Seq({ seq: cursor.seq });
case 'time':
return new GeneratedRussh.Cursor.TimeMs({ tMs: cursor.tMs });
case 'live':
return new GeneratedRussh.Cursor.Live();
}
switch (cursor.mode) {
case 'head':
return new GeneratedRussh.Cursor.Head();
case 'tailBytes':
return new GeneratedRussh.Cursor.TailBytes({
bytes: cursor.bytes,
});
case 'seq':
return new GeneratedRussh.Cursor.Seq({ seq: cursor.seq });
case 'time':
return new GeneratedRussh.Cursor.TimeMs({ tMs: cursor.tMs });
case 'live':
return new GeneratedRussh.Cursor.Live();
}
}
function toTerminalChunk(ch: GeneratedRussh.TerminalChunk): TerminalChunk {
return {
seq: ch.seq,
tMs: ch.tMs,
stream: streamEnumToLiteral[ch.stream],
bytes: ch.bytes,
};
return {
seq: ch.seq,
tMs: ch.tMs,
stream: streamEnumToLiteral[ch.stream],
bytes: ch.bytes,
};
}
function wrapShellSession(
shell: GeneratedRussh.ShellSessionInterface
shell: GeneratedRussh.ShellSessionInterface,
): SshShell {
const info = shell.getInfo();
const info = shell.getInfo();
const readBuffer: SshShell['readBuffer'] = (cursor, maxBytes) => {
const res = shell.readBuffer(cursorToGenerated(cursor), maxBytes);
return {
chunks: res.chunks.map(toTerminalChunk),
nextSeq: res.nextSeq,
dropped: res.dropped,
} satisfies BufferReadResult;
};
const readBuffer: SshShell['readBuffer'] = (cursor, maxBytes) => {
const res = shell.readBuffer(cursorToGenerated(cursor), maxBytes);
return {
chunks: res.chunks.map(toTerminalChunk),
nextSeq: res.nextSeq,
dropped: res.dropped,
} satisfies BufferReadResult;
};
const addListener: SshShell['addListener'] = (cb, opts) => {
const listener = {
onEvent: (ev: GeneratedRussh.ShellEvent) => {
if (ev instanceof GeneratedRussh.ShellEvent.Chunk) {
cb(toTerminalChunk(ev.inner[0]!));
} else if (ev instanceof GeneratedRussh.ShellEvent.Dropped) {
cb({
kind: 'dropped',
fromSeq: ev.inner.fromSeq,
toSeq: ev.inner.toSeq,
});
}
},
} satisfies GeneratedRussh.ShellListener;
const addListener: SshShell['addListener'] = (cb, opts) => {
const listener = {
onEvent: (ev: GeneratedRussh.ShellEvent) => {
if (ev instanceof GeneratedRussh.ShellEvent.Chunk) {
cb(toTerminalChunk(ev.inner[0]!));
} else if (ev instanceof GeneratedRussh.ShellEvent.Dropped) {
cb({
kind: 'dropped',
fromSeq: ev.inner.fromSeq,
toSeq: ev.inner.toSeq,
});
}
},
} satisfies GeneratedRussh.ShellListener;
try {
const id = shell.addListener(listener, {
cursor: cursorToGenerated(opts.cursor),
coalesceMs: opts.coalesceMs,
});
if (id === 0n) {
throw new Error('Failed to attach shell listener (id=0)');
}
return id;
} catch (e) {
throw new Error(
`addListener failed: ${String((e as any)?.message ?? e)}`
);
}
};
try {
const id = shell.addListener(listener, {
cursor: cursorToGenerated(opts.cursor),
coalesceMs: opts.coalesceMs,
});
if (id === 0n) {
throw new Error('Failed to attach shell listener (id=0)');
}
return id;
} catch (e) {
throw new Error(
`addListener failed: ${String((e as any)?.message ?? e)}`,
);
}
};
return {
channelId: info.channelId,
createdAtMs: info.createdAtMs,
pty: terminalTypeEnumToLiteral[info.term],
connectionId: info.connectionId,
sendData: (data, o) =>
shell.sendData(data, o?.signal ? { signal: o.signal } : undefined),
close: (o) => shell.close(o?.signal ? { signal: o.signal } : undefined),
// setBufferPolicy,
bufferStats: shell.bufferStats,
currentSeq: () => Number(shell.currentSeq()),
readBuffer,
addListener,
removeListener: (id) => shell.removeListener(id),
};
return {
channelId: info.channelId,
createdAtMs: info.createdAtMs,
pty: terminalTypeEnumToLiteral[info.term],
connectionId: info.connectionId,
sendData: (data, o) =>
shell.sendData(data, o?.signal ? { signal: o.signal } : undefined),
close: (o) => shell.close(o?.signal ? { signal: o.signal } : undefined),
// setBufferPolicy,
bufferStats: shell.bufferStats,
currentSeq: () => Number(shell.currentSeq()),
readBuffer,
addListener,
removeListener: (id) => shell.removeListener(id),
};
}
function wrapConnection(
conn: GeneratedRussh.SshConnectionInterface
conn: GeneratedRussh.SshConnectionInterface,
): SshConnection {
const info = conn.getInfo();
return {
connectionId: info.connectionId,
connectionDetails: generatedConnDetailsToIdeal(info.connectionDetails),
createdAtMs: info.createdAtMs,
connectedAtMs: info.connectedAtMs,
progressTimings: {
tcpEstablishedAtMs: info.progressTimings.tcpEstablishedAtMs,
sshHandshakeAtMs: info.progressTimings.sshHandshakeAtMs,
},
startShell: async ({ onClosed, ...params }) => {
const shell = await conn.startShell(
{
term: terminalTypeLiteralToEnum[params.term],
onClosedCallback: onClosed
? {
onChange: (channelId) => onClosed(channelId),
}
: undefined,
terminalMode: params.terminalMode,
terminalPixelSize: params.terminalPixelSize,
terminalSize: params.terminalSize,
},
params.abortSignal ? { signal: params.abortSignal } : undefined
);
return wrapShellSession(shell);
},
disconnect: (opts) =>
conn.disconnect(opts?.signal ? { signal: opts.signal } : undefined),
};
const info = conn.getInfo();
return {
connectionId: info.connectionId,
connectionDetails: generatedConnDetailsToIdeal(info.connectionDetails),
createdAtMs: info.createdAtMs,
connectedAtMs: info.connectedAtMs,
progressTimings: {
tcpEstablishedAtMs: info.progressTimings.tcpEstablishedAtMs,
sshHandshakeAtMs: info.progressTimings.sshHandshakeAtMs,
},
startShell: async ({ onClosed, ...params }) => {
const shell = await conn.startShell(
{
term: terminalTypeLiteralToEnum[params.term],
onClosedCallback: onClosed
? {
onChange: (channelId) => onClosed(channelId),
}
: undefined,
terminalMode: params.terminalMode,
terminalPixelSize: params.terminalPixelSize,
terminalSize: params.terminalSize,
},
params.abortSignal ? { signal: params.abortSignal } : undefined,
);
return wrapShellSession(shell);
},
disconnect: (opts) =>
conn.disconnect(opts?.signal ? { signal: opts.signal } : undefined),
};
}
async function connect({
onServerKey,
onConnectionProgress,
onDisconnected,
...options
onServerKey,
onConnectionProgress,
onDisconnected,
...options
}: ConnectOptions): Promise<SshConnection> {
const security =
options.security.type === 'password'
? new GeneratedRussh.Security.Password({
password: options.security.password,
})
: new GeneratedRussh.Security.Key({
privateKeyContent: options.security.privateKey,
});
const sshConnection = await GeneratedRussh.connect(
{
connectionDetails: {
host: options.host,
port: options.port,
username: options.username,
security,
},
onConnectionProgressCallback: onConnectionProgress
? {
onChange: (statusEnum) =>
onConnectionProgress(sshConnProgressEnumToLiteral[statusEnum]),
}
: undefined,
onDisconnectedCallback: onDisconnected
? {
onChange: (connectionId) => onDisconnected(connectionId),
}
: undefined,
onServerKeyCallback: {
onChange: (serverKeyInfo) =>
onServerKey(serverKeyInfo, options.abortSignal),
},
},
options.abortSignal ? { signal: options.abortSignal } : undefined
);
return wrapConnection(sshConnection);
const security =
options.security.type === 'password'
? new GeneratedRussh.Security.Password({
password: options.security.password,
})
: new GeneratedRussh.Security.Key({
privateKeyContent: options.security.privateKey,
});
const sshConnection = await GeneratedRussh.connect(
{
connectionDetails: {
host: options.host,
port: options.port,
username: options.username,
security,
},
onConnectionProgressCallback: onConnectionProgress
? {
onChange: (statusEnum) =>
onConnectionProgress(sshConnProgressEnumToLiteral[statusEnum]),
}
: undefined,
onDisconnectedCallback: onDisconnected
? {
onChange: (connectionId) => onDisconnected(connectionId),
}
: undefined,
onServerKeyCallback: {
onChange: (serverKeyInfo) =>
onServerKey(serverKeyInfo, options.abortSignal),
},
},
options.abortSignal ? { signal: options.abortSignal } : undefined,
);
return wrapConnection(sshConnection);
}
async function generateKeyPair(type: 'rsa' | 'ecdsa' | 'ed25519') {
const map = {
rsa: GeneratedRussh.KeyType.Rsa,
ecdsa: GeneratedRussh.KeyType.Ecdsa,
ed25519: GeneratedRussh.KeyType.Ed25519,
} as const;
return GeneratedRussh.generateKeyPair(map[type]);
const map = {
rsa: GeneratedRussh.KeyType.Rsa,
ecdsa: GeneratedRussh.KeyType.Ecdsa,
ed25519: GeneratedRussh.KeyType.Ed25519,
} as const;
return GeneratedRussh.generateKeyPair(map[type]);
}
function validatePrivateKey(
key: string
key: string,
):
| { valid: true; error?: never }
| { valid: false; error: GeneratedRussh.SshError } {
try {
GeneratedRussh.validatePrivateKey(key);
return { valid: true };
} catch (e) {
return { valid: false, error: e as GeneratedRussh.SshError };
}
| { valid: true; error?: never }
| { valid: false; error: GeneratedRussh.SshError } {
try {
GeneratedRussh.validatePrivateKey(key);
return { valid: true };
} catch (e) {
return { valid: false, error: e as GeneratedRussh.SshError };
}
}
// #endregion
@@ -413,8 +413,8 @@ function validatePrivateKey(
export { SshError, SshError_Tags } from './generated/uniffi_russh';
export const RnRussh = {
uniffiInitAsync: GeneratedRussh.uniffiInitAsync,
connect,
generateKeyPair,
validatePrivateKey,
uniffiInitAsync: GeneratedRussh.uniffiInitAsync,
connect,
generateKeyPair,
validatePrivateKey,
} satisfies RusshApi;