@opentui/ssh 0.0.0-20260612-c3be6d8c
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/README.md +350 -0
- package/index.js +988 -0
- package/index.js.map +20 -0
- package/package.json +31 -0
- package/src/auth.d.ts +97 -0
- package/src/banner.d.ts +14 -0
- package/src/bridge.d.ts +50 -0
- package/src/connection.d.ts +27 -0
- package/src/errors.d.ts +35 -0
- package/src/index.d.ts +4 -0
- package/src/keys.d.ts +18 -0
- package/src/logging.d.ts +40 -0
- package/src/run-session.d.ts +25 -0
- package/src/runtime.d.ts +38 -0
- package/src/safe.d.ts +23 -0
- package/src/server.d.ts +32 -0
- package/src/types.d.ts +294 -0
package/index.js.map
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/errors.ts", "../src/logging.ts", "../src/server.ts", "../src/keys.ts", "../src/banner.ts", "../src/bridge.ts", "../src/safe.ts", "../src/run-session.ts", "../src/connection.ts", "../src/auth.ts", "../src/runtime.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * The package's error vocabulary: {@link SshError} for failures and\n * {@link DenyError} for intentional session denial.\n */\n\n/**\n * Base for every failure this package throws. Carries a stable `code` so a\n * caller can branch without string-matching the message; `name` is the concrete\n * subclass (e.g. `\"ConfigError\"`).\n */\nexport class SshError extends Error {\n /** Stable, machine-branchable category — e.g. `\"CONFIG\"`. */\n readonly code: string\n constructor(code: string, message: string) {\n // Prefix with the package name: these are dev-facing (startup/`onError`),\n // never shown to a client, so the prefix leaks nothing. `DenyError` is the\n // exception — its message is the client-facing deny reason, so it stays plain.\n super(`@opentui/ssh: ${message}`)\n this.name = new.target.name\n this.code = code\n }\n}\n\n/**\n * A misconfiguration the developer must fix before the server can run (empty\n * credentials, an unparseable host key, a malformed `idleTimeout`). Thrown at\n * startup, never per-connection, so it surfaces when you wire the server up\n * rather than on a client's first connect.\n */\nexport class ConfigError extends SshError {\n constructor(message: string) {\n super(\"CONFIG\", message)\n }\n}\n\n/**\n * The control-flow signal a middleware's `session.deny()` throws to unwind the\n * chain — not a failure. `runSession` swallows it; anything that is not a\n * `DenyError` routes to `onError`.\n */\nexport class DenyError extends Error {\n /** The reason passed to `deny()`, if any — already delivered to the client. */\n readonly reason: string | undefined\n constructor(reason?: string) {\n super(reason ?? \"session denied\")\n this.name = \"DenyError\"\n this.reason = reason\n }\n}\n\n/** True for the deny control-flow signal — the one throw `runSession` swallows. */\nexport const isDeny = (err: unknown): err is DenyError => err instanceof DenyError\n",
|
|
6
|
+
"import type { Identity, Middleware, RemoteAddress } from \"./types.js\"\n\n/**\n * A connection-lifecycle event. Pure observability — the logging middleware\n * never reports errors (those flow to `onError`, the one error sink); it only\n * marks a session starting and ending.\n */\ninterface LogEventCommon<Id extends Identity> {\n /** Who connected, narrowed to the server's configured auth. */\n identity: Id\n remoteAddress: RemoteAddress\n term: string\n cols: number\n rows: number\n}\n\nexport type LogEvent<Id extends Identity = Identity> =\n | (LogEventCommon<Id> & { type: \"connect\"; durationMs?: never })\n | (LogEventCommon<Id> & { type: \"disconnect\"; durationMs: number })\n\nexport interface LoggingOptions<Id extends Identity = Identity> {\n /** Sink for events. Defaults to a one-line `console.log` formatter. */\n log?: (event: LogEvent<Id>) => void\n}\n\nconst formatAddress = (address: RemoteAddress): string =>\n address.port != null ? `${address.address}:${address.port}` : address.address\n\nconst formatDuration = (ms: number): string => (ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`)\nconst escapeControls = (value: string): string =>\n value.replace(\n /[\\u0000-\\u001f\\u007f-\\u009f]/g,\n (character) => `\\\\u${character.charCodeAt(0).toString(16).padStart(4, \"0\")}`,\n )\n\n/** The default one-line rendering, e.g. `connect alice@1.2.3.4:54321 publickey SHA256:… xterm-256color 80×24`. */\nfunction formatLogEvent(event: LogEvent): string {\n const who = `${escapeControls(event.identity.username)}@${formatAddress(event.remoteAddress)}`\n if (event.type === \"connect\") {\n const method =\n event.identity.method === \"publickey\" ? `publickey ${event.identity.fingerprint}` : event.identity.method\n return `connect ${who} ${method} ${escapeControls(event.term)} ${event.cols}×${event.rows}`\n }\n return `disconnect ${who} ${formatDuration(event.durationMs)}`\n}\n\n/**\n * Lifecycle logging as a `.use(...)` middleware: a \"connect\" event on entry, a\n * \"disconnect\" (with duration) on teardown. It is a setup/teardown onion — the\n * `finally` runs even when a downstream gate denies or the handler throws, so\n * every session is logged — but it only ever returns the handoff, never swallows\n * the throw. Errors stay the job of `onError`; this is observability alone.\n *\n * ```ts\n * createServer({ auth: { publicKey: \"any\" } })\n * .use(logging()) // default: one line per event to console.log\n * .serve((s) => mountApp(s.renderer))\n * ```\n */\nexport function logging<Id extends Identity = Identity>(options: LoggingOptions<Id> = {}): Middleware<Id> {\n const sink = options.log ?? ((event: LogEvent<Id>) => console.log(formatLogEvent(event)))\n const emit = (event: LogEvent<Id>) => {\n try {\n void Promise.resolve(sink(event)).catch(() => {})\n } catch {}\n }\n return async (session, next) => {\n const start = Date.now()\n emit({\n type: \"connect\",\n identity: session.identity,\n remoteAddress: session.remoteAddress,\n term: session.term,\n cols: session.cols,\n rows: session.rows,\n })\n try {\n return await next()\n } finally {\n emit({\n type: \"disconnect\",\n identity: session.identity,\n remoteAddress: session.remoteAddress,\n term: session.term,\n cols: session.cols,\n rows: session.rows,\n durationMs: Date.now() - start,\n })\n }\n }\n}\n",
|
|
7
|
+
"import ssh2 from \"ssh2\"\nimport { formatBanner } from \"./banner.js\"\nimport { createConnectionHandler } from \"./connection.js\"\nimport type { RuntimeMiddleware } from \"./run-session.js\"\nimport { resolveRuntime } from \"./runtime.js\"\nimport type {\n AuthConfig,\n AuthMethods,\n Identity,\n IdentityFor,\n ListenInfo,\n MiddlewareFunction,\n Server,\n ServerBuilder,\n ServerConfig,\n SessionHandler,\n} from \"./types.js\"\n\nconst { Server: Ssh2Server } = ssh2\n\n/** Loopback listeners skip the no-auth exposure warning. */\nconst isLoopback = (h: string) => h === \"127.0.0.1\" || h === \"::1\" || h === \"localhost\"\n\n/**\n * Build the running server. Reached only via the builder's `serve()`, so the\n * handler is always present. `resolveRuntime` derives everything from config\n * (host key, authenticator, idle budget, banner) and `createConnectionHandler`\n * owns the per-connection ssh2 lifecycle; this wires them into the ssh2 `Server`\n * and the `listen`/`close` lifecycle.\n */\nfunction buildServer<Id extends Identity>(\n config: ServerConfig<AuthConfig>,\n middlewares: RuntimeMiddleware<Id>[],\n handler: SessionHandler<Id>,\n): Server {\n const runtime = resolveRuntime(config)\n const connectionHandler = createConnectionHandler({\n authenticator: runtime.authenticator,\n middlewares: middlewares as RuntimeMiddleware[],\n handler: handler as SessionHandler,\n safe: runtime.safe,\n idleTimeoutMs: runtime.idleTimeoutMs,\n maxTimeoutMs: runtime.maxTimeoutMs,\n sessionLimits: runtime.sessionLimits,\n })\n\n const sshServer = new Ssh2Server({ hostKeys: runtime.hostKeys }, connectionHandler.onConnection)\n let reportsServerErrors = false\n let bindingAttempts = 0\n sshServer.on(\"error\", (err: Error) => {\n if (bindingAttempts === 0 && reportsServerErrors) runtime.safe.report(err)\n })\n\n return {\n listen(port = 2222, host = \"127.0.0.1\") {\n return new Promise<ListenInfo>((resolve, reject) => {\n // Convenience default is no auth; warn (don't block) when that combines\n // with a listener outside localhost. This is only a heuristic: containers,\n // tunnels, and proxies can expose localhost listeners too.\n if (runtime.noneOnly && !isLoopback(host)) {\n console.warn(\n `@opentui/ssh: no authentication configured while listening on ${host}. ` +\n \"Anyone who can reach this port, including through published container ports, tunnels, \" +\n \"or proxies, gets a session. Set `auth` to restrict access.\",\n )\n }\n bindingAttempts++\n const onError = (err: Error) => {\n bindingAttempts--\n reject(err)\n }\n sshServer.once(\"error\", onError)\n try {\n sshServer.listen(port, host, () => {\n bindingAttempts--\n sshServer.removeListener(\"error\", onError)\n reportsServerErrors = true\n connectionHandler.setAccepting(true)\n const addressInfo = sshServer.address()\n const actualPort = typeof addressInfo === \"object\" && addressInfo ? addressInfo.port : port\n const boundHost = typeof addressInfo === \"object\" && addressInfo ? addressInfo.address : host\n const info: ListenInfo = { host: boundHost, port: actualPort, fingerprints: runtime.fingerprints }\n if (config.startupBanner !== false) {\n console.log(formatBanner(info, runtime.banner).join(\"\\n\"))\n }\n resolve(info)\n })\n } catch (error) {\n bindingAttempts--\n sshServer.removeListener(\"error\", onError)\n reject(error)\n }\n })\n },\n async close() {\n await connectionHandler.closeAll()\n return new Promise<void>((resolve) => {\n sshServer.close(() => resolve())\n })\n },\n }\n}\n\n/**\n * The immutable builder behind `createServer`. Each `.use(mw)` returns a NEW\n * builder over the appended chain, re-typed so `Ctx` accumulates this link's\n * contribution; `.serve(handler)` seals it. The only state is the growing\n * middleware array threaded through.\n */\nfunction makeBuilder<Id extends Identity, Ctx extends object>(\n config: ServerConfig<AuthConfig>,\n middlewares: RuntimeMiddleware<Id>[],\n): ServerBuilder<Id, Ctx> {\n return {\n use<Add extends object>(mw: MiddlewareFunction<Id, Ctx, Add>): ServerBuilder<Id, Ctx & Add> {\n // The cast is the erasure boundary: `MiddlewareFunction` proved the types at the\n // call site; the chain runs the link erased.\n return makeBuilder<Id, Ctx & Add>(config, [...middlewares, mw as unknown as RuntimeMiddleware<Id>])\n },\n serve(handler: SessionHandler<Id, Ctx>): Server {\n return buildServer<Id>(config, middlewares, handler as SessionHandler<Id>)\n },\n }\n}\n\n/**\n * Create a server builder. Configure auth/host-key/etc. in the config object,\n * then chain `.use(mw)` for each cross-cutting concern and seal with\n * `serve(handler)`. `serve` takes the handler so the builder can flow the typed\n * `context` the chain accumulates (via `next({ ... })`) into it; the builder has\n * no `listen`, so a missing handler is a compile error.\n *\n * Inline middleware need no type arguments: `identity`/`context` flow from the\n * builder, the contribution is inferred from `next({ ... })`. Author a reusable one\n * by typing it as {@link Middleware} (or {@link MiddlewareFunction} when it reads\n * upstream context). `.use(...)` order is execution order, first is OUTERMOST.\n * `session.context` is the accumulation of every link's contribution.\n *\n * ```ts\n * const server = createServer({ auth: { publicKey: \"any\" } })\n * .use(async (s, next) => {\n * const user = await lookup(s.identity.fingerprint)\n * if (!user) s.deny(\"unknown key\")\n * return next({ user })\n * })\n * .serve((s) => mountApp(s.renderer, s.context.user))\n * await server.listen()\n * ```\n */\n// Specific overloads preserve inline contextual typing; the generic overload\n// accepts already-typed config objects. All start with an empty `{}` context.\nexport function createServer(\n config?: Omit<ServerConfig, \"auth\"> & { auth?: \"open\" },\n): ServerBuilder<IdentityFor<\"open\">>\nexport function createServer<A extends AuthMethods>(\n config: Omit<ServerConfig, \"auth\"> & { auth: A },\n): ServerBuilder<IdentityFor<A>>\nexport function createServer<A extends AuthConfig>(config: ServerConfig<A>): ServerBuilder<IdentityFor<A | \"open\">>\nexport function createServer<A extends AuthConfig = \"open\">(\n config: ServerConfig<A> = {},\n): ServerBuilder<IdentityFor<A>> {\n return makeBuilder<IdentityFor<A>, {}>(config, [])\n}\n",
|
|
8
|
+
"import { createHash, randomUUID } from \"node:crypto\"\nimport { existsSync, linkSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from \"node:fs\"\nimport { dirname } from \"node:path\"\nimport ssh2, { type ParsedKey } from \"ssh2\"\nimport { ConfigError } from \"./errors.js\"\nimport type { ServerConfig } from \"./types.js\"\n\nconst { utils } = ssh2\n\n/** SSH key helpers: fingerprinting, single-key parse normalization, and host-key resolution. */\n\nconst HOST_KEYGEN_ATTEMPTS = 20\nconst isKeyInput = (value: unknown): value is string | Buffer => typeof value === \"string\" || Buffer.isBuffer(value)\n\n/** OpenSSH-style SHA256 fingerprint of a raw public-key blob (`ssh-keygen -lf` form, e.g. `SHA256:nThbg6kX…`). */\nexport function sha256Fingerprint(publicKeyBlob: Buffer): string {\n const digest = createHash(\"sha256\").update(publicKeyBlob).digest(\"base64\")\n return `SHA256:${digest.replace(/=+$/, \"\")}`\n}\n\n/**\n * Parse a single SSH key from any form `utils.parseKey` accepts (PEM,\n * `authorized_keys` line, PPK), returning `null` on a parse error so callers\n * choose their own failure. parseKey returns an array for multi-key inputs;\n * this narrows to the first key.\n */\nexport function parseOneKey(input: string | Buffer): ParsedKey | null {\n const parsed = utils.parseKey(input)\n if (parsed instanceof Error) return null\n return Array.isArray(parsed) ? parsed[0]! : parsed\n}\n\nfunction generateParseableHostKey(): string {\n for (let i = 0; i < HOST_KEYGEN_ATTEMPTS; i++) {\n const pair = utils.generateKeyPairSync(\"ed25519\")\n if (parseOneKey(pair.private)) return pair.private\n }\n throw new ConfigError(\"could not generate a parseable ed25519 host key\")\n}\n\n/** Resolve host-key PEM(s) + fingerprints: explicit PEM, persisted path, or ephemeral. */\nexport function resolveHostKey(config: Pick<ServerConfig, \"hostKey\">): {\n hostKeyPems: (string | Buffer)[]\n fingerprints: string[]\n algorithms: string[]\n source: string\n} {\n const hostKey = config.hostKey\n let hostKeyPems: (string | Buffer)[]\n let source: string\n\n if (hostKey === undefined) {\n hostKeyPems = [generateParseableHostKey()]\n source = \"ephemeral\"\n } else if (!hostKey || typeof hostKey !== \"object\" || Array.isArray(hostKey)) {\n throw new ConfigError(\"hostKey must contain either path or pem\")\n } else if (\"pem\" in hostKey) {\n if (\"path\" in hostKey) throw new ConfigError(\"hostKey must contain either path or pem, not both\")\n if (!(isKeyInput(hostKey.pem) || (Array.isArray(hostKey.pem) && hostKey.pem.every(isKeyInput)))) {\n throw new ConfigError(\"hostKey.pem must be a key or array of keys\")\n }\n hostKeyPems = Array.isArray(hostKey.pem) ? hostKey.pem : [hostKey.pem]\n source = \"provided\"\n } else if (\"path\" in hostKey) {\n if (typeof hostKey.path !== \"string\" || hostKey.path.length === 0) {\n throw new ConfigError(\"hostKey.path must be a non-empty string\")\n }\n if (existsSync(hostKey.path)) {\n hostKeyPems = [readFileSync(hostKey.path)]\n source = `loaded ${hostKey.path}`\n } else {\n // First run: generate an ed25519 key, persist it owner-only, and use it.\n // POSIX-only hardening (dir 0700, key 0600), mirroring charmbracelet/keygen.\n // Windows has no POSIX mode bits, so there the key inherits the directory ACL.\n const pem = generateParseableHostKey()\n mkdirSync(dirname(hostKey.path), { recursive: true, mode: 0o700 })\n const temporaryPath = `${hostKey.path}.${process.pid}.${randomUUID()}.tmp`\n try {\n writeFileSync(temporaryPath, pem, { mode: 0o600, flag: \"wx\" })\n try {\n linkSync(temporaryPath, hostKey.path)\n hostKeyPems = [pem]\n source = `generated ${hostKey.path}`\n } catch (error) {\n if (!(error instanceof Error) || !(\"code\" in error) || error.code !== \"EEXIST\") throw error\n hostKeyPems = [readFileSync(hostKey.path)]\n source = `loaded ${hostKey.path}`\n }\n } finally {\n try {\n unlinkSync(temporaryPath)\n } catch {\n // The temporary file may not exist if creation failed.\n }\n }\n }\n } else {\n throw new ConfigError(\"hostKey must contain either path or pem\")\n }\n\n const keys = hostKeyPems.map((pem) => parseOneKey(pem))\n if (keys.length === 0) throw new ConfigError(\"hostKey.pem must contain at least one host key\")\n if (keys.some((key) => !key)) throw new ConfigError(`could not parse host key (${source})`)\n return {\n hostKeyPems,\n fingerprints: keys.map((key) => sha256Fingerprint(key!.getPublicSSH() as Buffer)),\n algorithms: keys.map((key) => key!.type),\n source,\n }\n}\n",
|
|
9
|
+
"import { sha256Fingerprint } from \"./keys.js\"\nimport type { ListenInfo } from \"./types.js\"\n\n/** Data a startup banner is rendered from; a function of config alone. */\nexport interface BannerDescriptor {\n /** Host-key algorithms in fingerprint order. */\n algorithms: string[]\n /** Where the host key came from: \"provided\" / \"ephemeral\" / \"loaded …\" / \"generated …\". */\n source: string\n /** Advertised auth methods, in banner order. */\n methods: string[]\n /** Base64 public-SSH blobs of the static allowlist; sampled for display. */\n authorizedKeys?: Set<string>\n}\n\n/** Startup-summary lines for `listen()`: pure formatting over a {@link BannerDescriptor} and bind. */\nexport function formatBanner(info: ListenInfo, descriptor: BannerDescriptor): string[] {\n const displayHost = info.host === \"0.0.0.0\" || info.host === \"::\" ? \"localhost\" : info.host\n const urlHost = displayHost.includes(\":\") ? `[${displayHost}]` : displayHost\n const lines = [\n `@opentui/ssh ▸ ssh://${urlHost}:${info.port}`,\n ...info.fingerprints.map(\n (fingerprint, index) => `host key ${fingerprint} (${descriptor.algorithms[index]}, ${descriptor.source})`,\n ),\n `auth ${descriptor.methods.join(\", \")}`,\n ]\n if (descriptor.authorizedKeys?.size) {\n const fps = [...descriptor.authorizedKeys].slice(0, 3).map((b64) => sha256Fingerprint(Buffer.from(b64, \"base64\")))\n const more = descriptor.authorizedKeys.size > fps.length ? \" …\" : \"\"\n lines.push(`authorized ${descriptor.authorizedKeys.size} keys · ${fps.join(\" \")}${more}`)\n }\n return lines\n}\n",
|
|
10
|
+
"import { Readable, Writable } from \"node:stream\"\nimport { CliRenderEvents, createCliRenderer, type CliRenderer } from \"@opentui/core\"\nimport type { ServerChannel } from \"ssh2\"\nimport { DenyError } from \"./errors.js\"\nimport { ignoreErrors, type SafeInvoke } from \"./safe.js\"\nimport type { Identity, MiddlewareSession, RemoteAddress, Session, SessionHandler } from \"./types.js\"\n\n/** Renderer factory; injectable for renderer creation and disconnect-race tests. */\nexport type RendererFactory = (options: Parameters<typeof createCliRenderer>[0]) => Promise<CliRenderer>\n\n/** PTY parameters from the client's `pty-req`; the renderer sizes off cols/rows. */\nexport interface PtyInfo {\n term: string\n cols: number\n rows: number\n hasPty: boolean\n}\n\nexport const DEFAULT_PTY: PtyInfo = { term: \"xterm-256color\", cols: 80, rows: 24, hasPty: false }\nexport const MAX_PTY = { cols: 500, rows: 200 } as const\nconst TRANSPORT_DRAIN_TIMEOUT_MS = 1_000\n\nconst UNKNOWN_REMOTE_ADDRESS: RemoteAddress = { address: \"unknown\" }\n\nfunction clampPtyDimension(value: number, fallback: number, max: number): number {\n if (!Number.isFinite(value) || value <= 0) return fallback\n const integer = Math.floor(value)\n return integer > 0 ? Math.min(integer, max) : fallback\n}\n\nfunction normalizePtyInfo(pty: PtyInfo): PtyInfo {\n return {\n term: pty.term || DEFAULT_PTY.term,\n cols: clampPtyDimension(pty.cols, DEFAULT_PTY.cols, MAX_PTY.cols),\n rows: clampPtyDimension(pty.rows, DEFAULT_PTY.rows, MAX_PTY.rows),\n hasPty: pty.hasPty,\n }\n}\n\n/**\n * Adapter stream pair for the renderer:\n * - stdin: a flowing Readable; raw client bytes from the channel are pushed in.\n * - stdout: a Writable the renderer's NativeSpanFeed writes frames to.\n */\nfunction createSessionStreams(channel: ServerChannel, cols: number, rows: number, onActivity?: () => void) {\n let inputPaused = false\n const stdin = new Readable({\n read() {\n if (!inputPaused) return\n inputPaused = false\n channel.resume()\n },\n })\n const onData = (chunk: Buffer) => {\n onActivity?.() // client input resets the idle-timeout clock\n if (!stdin.push(chunk) && !inputPaused) {\n inputPaused = true\n channel.pause()\n }\n }\n channel.on(\"data\", onData)\n\n let channelGone = false\n let pendingDrain: (() => void) | null = null\n // A deferred write will never drain after the peer vanishes.\n const releasePending = () => {\n const done = pendingDrain\n pendingDrain = null\n done?.()\n }\n channel.on(\"close\", () => {\n channelGone = true\n releasePending()\n })\n channel.on(\"error\", () => {\n channelGone = true\n releasePending()\n })\n\n const stdout = new Writable({\n write(chunk: Buffer | string, _enc, cb) {\n if (channelGone) return cb()\n // Copy renderer frame memory before acknowledging the write.\n const bytes = Buffer.from(chunk)\n if (bytes.byteLength === 0) return cb()\n // channel.write() returns false under backpressure; defer cb() to 'drain'\n // so flow control is applied back onto the feed instead of dropping frames.\n const ok = channel.write(bytes)\n if (ok) return cb()\n pendingDrain = cb\n channel.once(\"drain\", releasePending)\n },\n }) as unknown as NodeJS.WriteStream\n stdout.columns = cols\n stdout.rows = rows\n\n return { stdin: stdin as unknown as NodeJS.ReadStream, stdout, detach: () => channel.removeListener(\"data\", onData) }\n}\n\nexport interface SessionBridge {\n /** One runtime object exposed through middleware and handler session views. */\n session: Session & MiddlewareSession\n /** True once the session has closed (disconnect, end(), deny(), or idle reap). */\n readonly closed: boolean\n /**\n * Attach the renderer after middleware authorizes, run the handler, and resolve\n * when the session closes.\n */\n enterApp(handler: SessionHandler): Promise<void>\n resize(cols: number, rows: number): void\n destroy(): Promise<void>\n}\n\n/** What `createSessionBridge` needs to wire one ssh2 shell channel into a session. */\nexport interface SessionBridgeOptions {\n pty: PtyInfo\n identity: Identity\n idleTimeoutMs: number | undefined\n maxTimeoutMs: number | undefined\n safe: SafeInvoke\n /** Injectable renderer factory; defaults to `createCliRenderer` (tests drive the race/failure paths). */\n createRenderer?: RendererFactory\n remoteAddress?: RemoteAddress\n}\n\n/**\n * Turn an ssh2 shell channel into a wired-up OpenTUI session.\n *\n * The session starts without a renderer; `enterApp()` creates it only after the\n * middleware chain reaches the handler. The throwing getter catches JS callers and\n * unsafe casts that touch `session.renderer` too early.\n */\nexport function createSessionBridge(channel: ServerChannel, options: SessionBridgeOptions): SessionBridge {\n const {\n pty,\n identity,\n idleTimeoutMs,\n maxTimeoutMs,\n safe,\n createRenderer = createCliRenderer,\n remoteAddress = UNKNOWN_REMOTE_ADDRESS,\n } = options\n const initialPty = normalizePtyInfo(pty)\n // Assigned after `destroy` exists; the stream activity hook calls through it.\n let resetIdle = () => {}\n const { stdin, stdout, detach } = createSessionStreams(channel, initialPty.cols, initialPty.rows, () => resetIdle())\n\n // Created only if middleware reaches the handler.\n let renderer: CliRenderer | undefined\n\n let cols = initialPty.cols\n let rows = initialPty.rows\n // Per-session context bag filled by middleware `next(add)` calls.\n const context: Record<string, unknown> = {}\n const resizeListeners = new Set<(cols: number, rows: number) => void>()\n const closeListeners = new Set<() => void>()\n\n let closed = false\n let channelClosed = false // set when the client hung up — don't poke a dead channel\n let stdoutFinished = false\n let pendingRawWrites = 0\n let transportCloseTimer: ReturnType<typeof setTimeout> | undefined\n let resolveTransportClosed!: () => void\n const transportClosed = new Promise<void>((resolve) => {\n resolveTransportClosed = resolve\n })\n let idleTimer: ReturnType<typeof setTimeout> | undefined\n let maxTimer: ReturnType<typeof setTimeout> | undefined\n\n // One object backing both public session views.\n const session: Session & MiddlewareSession = {\n get renderer() {\n if (!renderer) {\n throw new Error(\n \"@opentui/ssh: session.renderer is unavailable until the handler runs — a middleware must call next() before using it\",\n )\n }\n return renderer\n },\n identity,\n context,\n term: initialPty.term,\n hasPty: initialPty.hasPty,\n remoteAddress,\n get cols() {\n return cols\n },\n get rows() {\n return rows\n },\n onResize(listener) {\n if (closed) return () => {}\n resizeListeners.add(listener)\n return () => resizeListeners.delete(listener)\n },\n onClose(listener) {\n // Late subscribers still get the close callback once.\n if (closed) {\n safe(listener)\n return () => {}\n }\n closeListeners.add(listener)\n return () => closeListeners.delete(listener)\n },\n write(data) {\n if (closed) return\n pendingRawWrites++\n channel.write(data, () => {\n pendingRawWrites--\n finishTransportClose()\n })\n },\n end() {\n void destroy()\n },\n deny(reason): never {\n // Keep deny reasons on the main screen by writing before the renderer exists.\n if (reason && !closed) {\n session.write(/\\r?\\n$/.test(reason) ? reason : `${reason}\\r\\n`)\n }\n void destroy()\n throw new DenyError(reason)\n },\n }\n\n const resize = (requestedCols: number, requestedRows: number) => {\n if (closed) return\n // Clamp each axis independently so one bad value does not discard the other.\n const nextCols = clampPtyDimension(requestedCols, cols, MAX_PTY.cols)\n const nextRows = clampPtyDimension(requestedRows, rows, MAX_PTY.rows)\n cols = nextCols\n rows = nextRows\n stdout.columns = nextCols\n stdout.rows = nextRows\n renderer?.resize(nextCols, nextRows)\n resizeListeners.forEach((listener) => safe(() => listener(nextCols, nextRows)))\n }\n\n // All session-ending paths funnel through this idempotent teardown.\n const settleTransportClosed = () => {\n if (transportCloseTimer) clearTimeout(transportCloseTimer)\n resolveTransportClosed()\n }\n\n const closeTransport = () => {\n if (channelClosed) return settleTransportClosed()\n ignoreErrors(() => channel.exit(0))\n ignoreErrors(() => channel.close())\n settleTransportClosed()\n }\n\n const finishTransportClose = () => {\n if (!closed || !stdoutFinished || pendingRawWrites > 0 || channelClosed) return\n closeTransport()\n }\n\n const destroy = (): Promise<void> => {\n if (closed) return transportClosed\n closed = true\n if (idleTimer) clearTimeout(idleTimer)\n if (maxTimer) clearTimeout(maxTimer)\n ignoreErrors(() => renderer?.destroy())\n if (!channelClosed) {\n transportCloseTimer = setTimeout(closeTransport, TRANSPORT_DRAIN_TIMEOUT_MS)\n // Core can enqueue final terminal-restoration bytes during destroy. End the\n // adapter on the next microtask and let its pending writes drain before close.\n queueMicrotask(() => {\n stdout.end(() => {\n stdoutFinished = true\n finishTransportClose()\n })\n })\n }\n detach()\n closeListeners.forEach((listener) => safe(listener))\n if (channelClosed) settleTransportClosed()\n return transportClosed\n }\n\n // Reap a session that goes quiet for too long. Armed at start and re-armed on\n // every client keystroke; cleared on close so a reaped/closed session can't fire.\n if (idleTimeoutMs && idleTimeoutMs > 0) {\n resetIdle = () => {\n if (closed) return\n if (idleTimer) clearTimeout(idleTimer)\n idleTimer = setTimeout(destroy, idleTimeoutMs)\n }\n resetIdle()\n }\n\n if (maxTimeoutMs && maxTimeoutMs > 0) {\n maxTimer = setTimeout(destroy, maxTimeoutMs)\n }\n\n // Mark the channel gone before teardown so we do not write to a dead peer.\n channel.on(\"close\", () => {\n channelClosed = true\n settleTransportClosed()\n void destroy()\n })\n channel.on(\"error\", (error: Error) => {\n channelClosed = true\n settleTransportClosed()\n safe.report(error)\n void destroy()\n })\n\n // Use current dimensions so resizes during middleware are honored.\n const attachRenderer = async (): Promise<CliRenderer | null> => {\n if (renderer) return renderer\n // The session may have closed while middleware was still running.\n if (closed) return null\n const createdRenderer = await createRenderer({\n stdin,\n stdout, // custom stdout → frames routed through NativeSpanFeed\n width: cols,\n height: rows,\n exitOnCtrlC: false, // the app/server owns quit; don't kill on ^C\n exitSignals: [], // no process-level signal handling for a remote peer\n consoleMode: \"disabled\", // never patch the host's global console per session\n targetFps: 30,\n })\n // The client may vanish while createRenderer is awaiting; release the renderer\n // immediately instead of attaching it to dead streams.\n if (closed) {\n ignoreErrors(() => createdRenderer.destroy())\n return null\n }\n if (createdRenderer.width !== cols || createdRenderer.height !== rows) {\n createdRenderer.resize(cols, rows)\n }\n createdRenderer.on(CliRenderEvents.DESTROY, destroy)\n renderer = createdRenderer\n return renderer\n }\n\n const enterApp = async (handler: SessionHandler): Promise<void> => {\n // Register before renderer setup so an early close cannot be missed.\n const ended = new Promise<void>((resolve) => session.onClose(resolve))\n if (closed) return ended\n let attachedRenderer: CliRenderer | null\n try {\n attachedRenderer = await attachRenderer()\n } catch (err) {\n destroy()\n throw err\n }\n if (!attachedRenderer) return ended\n const handlerDone = Promise.resolve()\n .then(() => handler(session))\n .then(\n () => ({ type: \"handler\" as const }),\n (err) => ({ type: \"handler-error\" as const, err }),\n )\n\n const outcome = await Promise.race([handlerDone, ended.then(() => ({ type: \"ended\" as const }))])\n if (outcome.type === \"handler-error\") throw outcome.err\n if (outcome.type === \"handler\") await ended\n if (outcome.type === \"ended\") {\n void handlerDone.then((late) => {\n if (late.type === \"handler-error\") safe.report(late.err)\n })\n }\n }\n\n return {\n session,\n get closed() {\n return closed\n },\n enterApp,\n resize,\n destroy,\n }\n}\n",
|
|
11
|
+
"/**\n * Isolation guard for user callbacks. Each runs synchronously inside an ssh2\n * event handler, where an uncaught throw or rejection would reach ssh2's emitter\n * and could drop the connection or the process, and would starve sibling\n * callbacks in the same dispatch.\n *\n * `safe(fn)` runs a callback and routes any throw/rejection to the sink; its\n * returned promise always resolves, so callers can await without their own guard.\n * `safe.report(err)` sinks an error with no callback (connection/server errors).\n * A throwing sink is contained too.\n */\nexport interface SafeInvoke {\n (fn: () => unknown): Promise<void>\n /** Report an error directly to the sink, without ever letting the sink throw escape. */\n report(err: unknown): void\n}\n\nexport function createSafeInvoke(onError: (err: unknown) => void): SafeInvoke {\n const report = (err: unknown) => {\n try {\n onError(err)\n } catch {\n // Last frame before ssh2's handler; a throwing sink cannot escape.\n }\n }\n\n const safe = async (fn: () => unknown): Promise<void> => {\n try {\n await fn()\n } catch (err) {\n report(err)\n }\n }\n\n return Object.assign(safe, { report })\n}\n\n/**\n * Best-effort teardown: run `fn` and swallow any throw. Distinct from `safe`:\n * this ignores the error rather than routing it to `onError`, for cleanup paths\n * (renderer/channel/socket destroy) where a failure is not worth reporting.\n */\nexport function ignoreErrors(fn: () => void): void {\n try {\n fn()\n } catch {}\n}\n",
|
|
12
|
+
"import type { SessionBridge } from \"./bridge.js\"\nimport { isDeny } from \"./errors.js\"\nimport type { SafeInvoke } from \"./safe.js\"\nimport type { Identity, MiddlewareSession, Next, SessionHandler } from \"./types.js\"\n\n/**\n * Erased middleware-chain type for the runtime. Type safety lives in the public\n * `ServerBuilder` interface; the impl runs with the contribution generic erased.\n */\nexport type RuntimeMiddleware<Id extends Identity = Identity> = (\n session: MiddlewareSession<Id>,\n next: Next,\n) => unknown | Promise<unknown>\n\n/**\n * Run the middleware onion around the handler for one session, then ensure the\n * session is closed once the chain settles.\n *\n * `dispatch()` invokes link `i`; the link continues by calling `next()`, which\n * recurses to link `i + 1`. `next(add)` merges `add` into the live per-session\n * context bag BEFORE advancing, so every downstream link and the handler read it.\n * The innermost `next()` reaches the leaf `bridge.enterApp(handler)`, which runs\n * the handler and resolves at teardown — so a link's post-`next()` code runs as\n * teardown. First `.use()` is the OUTERMOST link (use order === execution order).\n *\n * Settling means the session is over: normally at disconnect, or early when a link\n * calls `deny()` (swallowed here) or never calls `next()`. The `finally` closes the\n * session either way; `end()` is idempotent. A real (non-deny) throw reaches\n * `safe()` → `onError`.\n */\nexport function runSession(\n middlewares: RuntimeMiddleware[],\n handler: SessionHandler,\n bridge: SessionBridge,\n safe: SafeInvoke,\n): void {\n const session = bridge.session\n const context = session.context as Record<string, unknown>\n\n const dispatch = async (index: number): Promise<void> => {\n // The leaf owns the renderer lifecycle and resolves at teardown.\n if (index === middlewares.length) return bridge.enterApp(handler)\n const mw = middlewares[index]!\n // Calling next() twice would re-run the rest of the chain; reject it.\n // Contained by safe() → onError.\n let nextCalled = false\n const next = (add?: object): Promise<void> => {\n if (nextCalled) throw new Error(\"@opentui/ssh: next() called more than once in a single middleware\")\n nextCalled = true\n if (add) Object.assign(context, add)\n return dispatch(index + 1)\n }\n // Await the returned handoff so post-next teardown runs. The `Handoff` brand is erased here.\n await mw(session as MiddlewareSession, next as unknown as Next)\n }\n\n void safe(async () => {\n try {\n await dispatch(0)\n } catch (err) {\n // deny() already wrote the reason and closed the session; the throw only\n // unwinds the chain, so swallow it. Anything else is a real failure for safe().\n if (!isDeny(err)) throw err\n } finally {\n session.end()\n }\n })\n}\n",
|
|
13
|
+
"import type { ClientInfo, Connection } from \"ssh2\"\nimport type { Authenticator } from \"./auth.js\"\nimport { createSessionBridge, DEFAULT_PTY, type PtyInfo, type SessionBridge } from \"./bridge.js\"\nimport { type RuntimeMiddleware, runSession } from \"./run-session.js\"\nimport { ignoreErrors, type SafeInvoke } from \"./safe.js\"\nimport type { Identity, RemoteAddress, SessionHandler } from \"./types.js\"\nimport type { ResolvedSessionLimits } from \"./runtime.js\"\n\nconst SHUTDOWN_DRAIN_TIMEOUT_MS = 1_000\n\n/** What the connection handler needs from the resolved runtime and sealed chain. */\nexport interface ConnectionDependencies {\n authenticator: Authenticator\n middlewares: RuntimeMiddleware[]\n handler: SessionHandler\n safe: SafeInvoke\n idleTimeoutMs: number | undefined\n maxTimeoutMs: number | undefined\n sessionLimits: ResolvedSessionLimits\n}\n\nconst normalizeAddress = (address: string | undefined): string => {\n if (!address) return \"unknown\"\n return address.startsWith(\"::ffff:\") ? address.slice(\"::ffff:\".length) : address\n}\n\nconst toRemoteAddress = (address: string | undefined, port: number | undefined): RemoteAddress => ({\n address: normalizeAddress(address),\n ...(typeof port === \"number\" ? { port } : {}),\n})\n\n/**\n * The per-connection ssh2 authentication → session → shell lifecycle.\n * `onConnection` is the ssh2 `Server` connection listener; `closeAll` drains\n * tracked bridges then sockets for `Server.close()`. Bridge/client tracking\n * lives here because this is what spawns the sessions.\n */\nexport function createConnectionHandler(dependencies: ConnectionDependencies): {\n onConnection: (client: Connection, info: ClientInfo) => void\n closeAll: () => Promise<void>\n setAccepting: (accepting: boolean) => void\n} {\n const { authenticator, middlewares, handler, safe, idleTimeoutMs, maxTimeoutMs, sessionLimits } = dependencies\n const clients = new Set<Connection>()\n const bridges = new Map<SessionBridge, () => void>()\n let activeSessions = 0\n let acceptingSessions = false\n\n const onConnection = (client: Connection, info: ClientInfo) => {\n clients.add(client)\n let connected = true\n let connectionSessions = 0\n\n // Updated once authentication accepts a real identity.\n let identity: Identity = { method: \"none\", username: \"unknown\" }\n // Remote address from ssh2's public, typed `info`. (The local address isn't on\n // `ClientInfo` and ssh2 exposes no public accessor, so we don't surface it.)\n const remoteAddress = toRemoteAddress(info.ip, info.port)\n\n client.on(\"authentication\", async (ctx) => {\n const outcome = await authenticator.handle(ctx)\n if (!connected) return\n if (outcome.type === \"reject\") return ctx.reject(outcome.methods)\n if (outcome.type === \"accept\") identity = outcome.identity\n return ctx.accept()\n })\n\n client.on(\"ready\", () => {\n client.on(\"session\", (acceptSession) => {\n const sshSession = acceptSession()\n let pty: PtyInfo = DEFAULT_PTY\n let activeBridge: SessionBridge | undefined\n\n sshSession.on(\"pty\", (accept, _reject, info) => {\n // `term` typed via ssh2-augment.d.ts (@types/ssh2 omits it at this version).\n pty = {\n term: info.term ?? \"\",\n cols: info.cols,\n rows: info.rows,\n hasPty: true,\n }\n accept?.()\n })\n\n // SIGWINCH is process.stdout-only; forward ssh2 window-change manually.\n sshSession.on(\"window-change\", (accept, _reject, info) => {\n accept?.()\n activeBridge?.resize(info.cols, info.rows)\n })\n\n sshSession.on(\"shell\", (accept, reject) => {\n if (\n !acceptingSessions ||\n connectionSessions >= sessionLimits.perConnection ||\n activeSessions >= sessionLimits.global\n ) {\n reject?.()\n return\n }\n\n connectionSessions++\n activeSessions++\n let released = false\n const release = () => {\n if (released) return\n released = true\n connectionSessions--\n activeSessions--\n }\n\n let channel: ReturnType<typeof accept> | undefined\n try {\n channel = accept()\n // Keep each shell's teardown tied to its own bridge; `activeBridge` may\n // be replaced if the client opens another shell on the same SSH session.\n const shellBridge = createSessionBridge(channel, {\n pty,\n identity,\n idleTimeoutMs,\n maxTimeoutMs,\n safe,\n remoteAddress,\n })\n activeBridge = shellBridge\n bridges.set(shellBridge, release)\n shellBridge.session.onClose(() => {\n void shellBridge.destroy().finally(() => {\n bridges.delete(shellBridge)\n release()\n })\n })\n runSession(middlewares, handler, shellBridge, safe)\n } catch (error) {\n const acceptedChannel = channel\n if (acceptedChannel) ignoreErrors(() => acceptedChannel.close())\n release()\n safe.report(error)\n }\n })\n })\n })\n\n client.on(\"close\", () => {\n connected = false\n clients.delete(client)\n })\n client.on(\"error\", (err: Error) => safe.report(err))\n }\n\n const closeAll = async () => {\n acceptingSessions = false\n const draining = Promise.all([...bridges.keys()].map((bridge) => bridge.destroy()))\n let timeout: ReturnType<typeof setTimeout> | undefined\n await Promise.race([\n draining,\n new Promise<void>((resolve) => {\n timeout = setTimeout(resolve, SHUTDOWN_DRAIN_TIMEOUT_MS)\n }),\n ])\n if (timeout) clearTimeout(timeout)\n for (const release of bridges.values()) release()\n bridges.clear()\n for (const client of clients) {\n ignoreErrors(() => client.end())\n // Force the socket shut: client.end() is a graceful half-close that hangs\n // net.Server.close() if the peer is already gone.\n ignoreErrors(() => {\n ;(client as unknown as { _sock?: { destroy?: () => void } })._sock?.destroy?.()\n })\n }\n clients.clear()\n }\n\n return {\n onConnection,\n closeAll,\n setAccepting(accepting) {\n acceptingSessions = accepting\n },\n }\n}\n",
|
|
14
|
+
"import { readFileSync } from \"node:fs\"\nimport type { AuthContext } from \"ssh2\"\nimport { ConfigError } from \"./errors.js\"\nimport { parseOneKey, sha256Fingerprint } from \"./keys.js\"\nimport type { AuthConfig, CredentialMethods, Identity, KeyboardPrompt, PublicKey } from \"./types.js\"\n\n/**\n * Internal normalized policy the `Authenticator` decides over. `\"open\"` is\n * normalized to `{ none: true }`; an `AuthMethods` set passes through.\n */\nexport interface NormalizedAuthConfig extends CredentialMethods {\n /** Set by normalization when `auth` is `\"open\"`; never written by a user. */\n none?: boolean\n}\n\n/** The SSH auth methods this package understands. Subset of ssh2's `AuthenticationType`. */\nexport type AuthMethod = \"none\" | \"password\" | \"publickey\" | \"keyboard-interactive\"\n\n/**\n * A single auth attempt, narrowed to the fields a decision needs. This keeps the\n * auth core testable without a live ssh2 handshake.\n */\nexport type AuthAttempt =\n | { method: \"none\"; username: string }\n | { method: \"password\"; username: string; password: string }\n | {\n method: \"keyboard-interactive\"\n username: string\n prompt: KeyboardPrompt\n }\n | {\n method: \"publickey\"\n username: string\n key: PublicKey\n /** Present only on the signed (second) pass; absent on the probe. */\n signature?: Buffer\n /** The signed data; present alongside `signature`. */\n blob?: Buffer\n hashAlgo?: string\n }\n\ntype PublicKeyAttempt = Extract<AuthAttempt, { method: \"publickey\" }>\n\n/**\n * The verdict for one attempt. `acceptProbe` is the publickey query reply\n * (PK_OK): the key is acceptable, so the client should sign — no identity yet.\n * Only a verified attempt yields an `accept` with an `identity`.\n */\nexport type AuthOutcome =\n | { type: \"accept\"; identity: Identity }\n | { type: \"acceptProbe\" }\n | { type: \"reject\"; methods: AuthMethod[] }\n\n/**\n * Adapt ssh2's live `AuthContext` into a plain `AuthAttempt`. Read-only w.r.t.\n * ssh2 (never calls `ctx.accept()`/`reject()`), so the wiring is unit-testable\n * without a real handshake. Returns `null` for an unmodeled method so `handle()`\n * can reject it.\n */\nexport function attemptFromAuthContext(ctx: AuthContext): AuthAttempt | null {\n switch (ctx.method) {\n case \"none\":\n return { method: \"none\", username: ctx.username }\n case \"password\":\n return { method: \"password\", username: ctx.username, password: ctx.password }\n case \"keyboard-interactive\":\n return {\n method: \"keyboard-interactive\",\n username: ctx.username,\n // Bridge ssh2's callback-based ctx.prompt() onto our promise-based prompt().\n prompt: (questions) =>\n new Promise<string[]>((resolve) => {\n ctx.prompt(questions, (answers) => resolve(answers ?? []))\n }),\n }\n case \"publickey\":\n // The verifier checks `signature` over `blob` using `key`/`hashAlgo`; carry\n // them through unchanged. They are absent on the probe (first) pass and\n // present on the signed (second) pass.\n return {\n method: \"publickey\",\n username: ctx.username,\n key: { algorithm: ctx.key.algo, blob: ctx.key.data },\n signature: ctx.signature,\n blob: ctx.blob,\n hashAlgo: ctx.hashAlgo,\n }\n default:\n return null\n }\n}\n\nexport interface Authenticator {\n /**\n * Decide a live ssh2 auth context end to end: adapt, decide, and fail closed on\n * any unexpected throw. The connection handler just applies the verdict.\n */\n handle(ctx: AuthContext): Promise<AuthOutcome>\n /**\n * Decide a single already-adapted attempt. Value-in, value-out — the unit seam\n * for the security-critical paths (above all signature verification).\n */\n authenticate(attempt: AuthAttempt): Promise<AuthOutcome>\n /** The configured methods, told to clients on reject and in the banner. */\n advertisedMethods(): AuthMethod[]\n}\n\n/**\n * Reconstruct a `ParsedKey` from an attempt's `key` so we can verify its\n * signature ourselves: ssh2 surfaces only `{ algo, data }` with no `.verify()`.\n * `parseOneKey` accepts the OpenSSH one-line form rebuilt here.\n */\nfunction parseAttemptKey(key: PublicKey) {\n return parseOneKey(`${key.algorithm} ${key.blob.toString(\"base64\")}`)\n}\n\n/**\n * Build the auth decision core from normalized config and a pre-parsed allowlist.\n *\n * User predicates and signature verification run against client-supplied input.\n * Any throw is reported through `onError` and treated as a reject.\n */\nexport function createAuthenticator(\n auth: NormalizedAuthConfig,\n authorizedKeys?: Set<string>,\n onError: (err: unknown) => void = () => {},\n): Authenticator {\n // User predicates must opt in with `true`; throws are reported and rejected.\n const guard = async (fn: () => boolean | Promise<boolean>): Promise<boolean> => {\n try {\n return (await fn()) === true\n } catch (err) {\n onError(err)\n return false\n }\n }\n\n const advertisedMethods = (): AuthMethod[] => {\n const methods: AuthMethod[] = []\n if (auth.publicKey || authorizedKeys) methods.push(\"publickey\")\n if (auth.password) methods.push(\"password\")\n if (auth.keyboardInteractive) methods.push(\"keyboard-interactive\")\n if (auth.none) methods.push(\"none\")\n return methods\n }\n\n const reject = (): AuthOutcome => ({ type: \"reject\", methods: advertisedMethods() })\n\n const allowFn = typeof auth.publicKey === \"object\" ? auth.publicKey.allow : undefined\n const staticAdmits = (attempt: PublicKeyAttempt): boolean =>\n auth.publicKey === \"any\" || authorizedKeys?.has(attempt.key.blob.toString(\"base64\")) === true\n\n const authenticate = async (attempt: AuthAttempt): Promise<AuthOutcome> => {\n switch (attempt.method) {\n case \"none\":\n return auth.none ? { type: \"accept\", identity: { method: \"none\", username: attempt.username } } : reject()\n\n case \"password\": {\n const fn = auth.password\n if (!fn) return reject()\n const ok = await guard(() => fn({ username: attempt.username, password: attempt.password }))\n return ok ? { type: \"accept\", identity: { method: \"password\", username: attempt.username } } : reject()\n }\n\n case \"keyboard-interactive\": {\n const fn = auth.keyboardInteractive\n if (!fn) return reject()\n const ok = await guard(() => fn({ username: attempt.username, prompt: attempt.prompt }))\n return ok\n ? { type: \"accept\", identity: { method: \"keyboard-interactive\", username: attempt.username } }\n : reject()\n }\n\n case \"publickey\": {\n if (!auth.publicKey && !authorizedKeys) return reject()\n\n // First pass: tell the client whether this key is worth signing.\n if (!attempt.signature) {\n if (staticAdmits(attempt) || typeof allowFn === \"function\") return { type: \"acceptProbe\" }\n return reject()\n }\n const signature = attempt.signature\n\n // Signed pass only: fingerprint feeds the `allow` predicate and identity.\n const fingerprint = sha256Fingerprint(attempt.key.blob)\n\n // ssh2 provides the signature but does not verify it for us. Do not mint\n // an identity until the signature verifies over the signed blob.\n if (!attempt.blob) return reject()\n const blob = attempt.blob\n const parsed = parseAttemptKey(attempt.key)\n const verified = await guard(() => parsed?.verify(blob, signature, attempt.hashAlgo) === true)\n if (!verified) return reject()\n // Key proven; now apply the admission policy.\n if (!staticAdmits(attempt)) {\n if (!allowFn) return reject()\n const allowed = await guard(() =>\n allowFn({ username: attempt.username, fingerprint, publicKey: attempt.key }),\n )\n if (!allowed) return reject()\n }\n return {\n type: \"accept\",\n identity: { method: \"publickey\", username: attempt.username, fingerprint, publicKey: attempt.key },\n }\n }\n\n default: {\n // Exhaustiveness seam: adding an AuthAttempt method without a case is a\n // compile error. Still fails closed (reject) for an untyped caller that\n // slips a method past the type.\n const _exhaustive: never = attempt\n void _exhaustive\n return reject()\n }\n }\n }\n\n const handle = async (ctx: AuthContext): Promise<AuthOutcome> => {\n // An unmodeled method becomes a reject (with the advertised set), never an\n // attempt the core might accept.\n const attempt = attemptFromAuthContext(ctx)\n if (!attempt) return reject()\n try {\n return await authenticate(attempt)\n } catch (err) {\n // User predicates already fail closed via guard(); this catches an\n // unexpected throw so it can never escape into ssh2.\n onError(err)\n return reject()\n }\n }\n\n return { advertisedMethods, authenticate, handle }\n}\n\n/**\n * Read a set of base64'd public-SSH blobs from an authorized_keys file or array.\n * We parse each line (skipping blanks/comments) rather than hash it because\n * Node's `crypto.createPublicKey` doesn't accept the one-line `ssh-ed25519 AAAA…`\n * form; `parseOneKey` does, yielding a comparable blob.\n */\nfunction loadAuthorizedKeys(source: string | string[]): Set<string> {\n let lines: string[]\n if (typeof source === \"string\") {\n try {\n lines = readFileSync(source, \"utf8\").split(\"\\n\")\n } catch {\n throw new ConfigError(`could not read authorizedKeys file: ${source}`)\n }\n } else {\n lines = source\n }\n const set = new Set<string>()\n for (const raw of lines) {\n const line = raw.trim()\n if (!line || line.startsWith(\"#\")) continue\n const key = parseOneKey(line)\n if (!key) throw new ConfigError(`invalid authorizedKeys entry: ${line.slice(0, 40)}`)\n set.add(key.getPublicSSH().toString(\"base64\"))\n }\n if (set.size === 0) throw new ConfigError(\"authorizedKeys did not contain any public keys\")\n return set\n}\n\n/** The auth facts `resolveAuth` decides off the public `AuthConfig`. */\nexport interface ResolvedAuth {\n /** The security core, normalized off `auth` (+ any static allowlist). */\n authenticator: Authenticator\n /** True when `none` is the only advertised method — a wide-open server (listen() warns outside localhost). */\n noneOnly: boolean\n /** Parsed static allowlist, surfaced for the startup banner; undefined when none. */\n authorizedKeys: Set<string> | undefined\n}\n\n/**\n * Resolve the public `AuthConfig` into the running auth: normalize \"open\" to the\n * internal none-only config, parse the static allowlist once at startup, build the\n * decision core, and reject an empty credential set at startup.\n *\n * `onError` is the fail-closed sink threaded into the core (a throwing user\n * predicate is denied + reported, never leaked) and into `handle()`.\n */\nexport function resolveAuth(auth: AuthConfig | undefined, onError: (err: unknown) => void): ResolvedAuth {\n // \"open\" (or omitted) is the no-auth default, normalized to { none: true }; a\n // AuthMethods set passes through. The public `AuthConfig` sum forbids mixing the two.\n const isOpen = auth === undefined || auth === \"open\"\n if (!isOpen) {\n if (!auth || typeof auth !== \"object\" || Array.isArray(auth)) throw new ConfigError(\"invalid auth configuration\")\n if (\"none\" in auth) throw new ConfigError('auth.none is invalid — use auth: \"open\" for no authentication')\n if (auth.password !== undefined && typeof auth.password !== \"function\") {\n throw new ConfigError(\"auth.password must be a function\")\n }\n if (auth.keyboardInteractive !== undefined && typeof auth.keyboardInteractive !== \"function\") {\n throw new ConfigError(\"auth.keyboardInteractive must be a function\")\n }\n const publicKey = auth.publicKey\n if (publicKey !== undefined && publicKey !== \"any\") {\n if (!publicKey || typeof publicKey !== \"object\" || Array.isArray(publicKey)) {\n throw new ConfigError('auth.publicKey must be \"any\" or a policy object')\n }\n if (publicKey.allow !== undefined && typeof publicKey.allow !== \"function\") {\n throw new ConfigError(\"auth.publicKey.allow must be a function\")\n }\n const authorizedKeys = publicKey.authorizedKeys\n if (\n authorizedKeys !== undefined &&\n typeof authorizedKeys !== \"string\" &&\n !(Array.isArray(authorizedKeys) && authorizedKeys.every((key) => typeof key === \"string\"))\n ) {\n throw new ConfigError(\"auth.publicKey.authorizedKeys must be a path or array of public keys\")\n }\n }\n }\n const authConfig: NormalizedAuthConfig = isOpen ? { none: true } : auth\n\n // The static allowlist nests under publicKey; parse it once here, not per\n // connection. (The dynamic `allow` predicate stays in authConfig for the core to call.)\n const publicKeyPolicy = typeof authConfig.publicKey === \"object\" ? authConfig.publicKey : undefined\n if (publicKeyPolicy && !publicKeyPolicy.authorizedKeys && typeof publicKeyPolicy.allow !== \"function\") {\n throw new ConfigError('auth.publicKey must set \"any\", authorizedKeys, or allow')\n }\n const authorizedKeys = publicKeyPolicy?.authorizedKeys\n ? loadAuthorizedKeys(publicKeyPolicy.authorizedKeys)\n : undefined\n\n const authenticator = createAuthenticator(authConfig, authorizedKeys, onError)\n\n // Empty credentials configure no methods, so no client could authenticate.\n const methods = authenticator.advertisedMethods()\n if (!isOpen && methods.length === 0) {\n throw new ConfigError(\n \"auth: {} configures no authentication methods — no client could connect. \" +\n 'Set publicKey / password / keyboardInteractive, or use auth: \"open\" for no authentication.',\n )\n }\n\n // No-auth servers are wide open; listen() warns when one listens outside localhost.\n return { authenticator, noneOnly: isOpen, authorizedKeys }\n}\n",
|
|
15
|
+
"import { type Authenticator, resolveAuth } from \"./auth.js\"\nimport type { BannerDescriptor } from \"./banner.js\"\nimport { ConfigError } from \"./errors.js\"\nimport { resolveHostKey } from \"./keys.js\"\nimport { createSafeInvoke, type SafeInvoke } from \"./safe.js\"\nimport type { AuthConfig, ServerConfig } from \"./types.js\"\n\nconst MAX_DURATION_MS = 24 * 60 * 60 * 1_000\nconst DURATION_UNITS = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000 } as const\nconst DEFAULT_SESSION_LIMITS = { perConnection: 1, global: 100 } as const\n\n/** Parse a duration into milliseconds: a number is ms; \"10m\"/\"30s\"/\"500ms\" is unit-suffixed. */\nfunction parseDuration(name: string, value: string | number): number {\n let ms: number\n if (typeof value === \"number\") {\n ms = value\n } else {\n const match = /^(\\d+)\\s*(ms|s|m|h)?$/.exec(value.trim())\n if (!match) throw new ConfigError(`invalid ${name}: ${value}`)\n const unit = match[2] as keyof typeof DURATION_UNITS | undefined\n ms = Number(match[1]) * DURATION_UNITS[unit ?? \"ms\"]\n }\n if (!Number.isSafeInteger(ms) || ms <= 0 || ms > MAX_DURATION_MS) throw new ConfigError(`invalid ${name}: ${value}`)\n return ms\n}\n\nfunction parseLimit(name: string, value: number | undefined, fallback: number): number {\n const limit = value === undefined ? fallback : value\n if (!Number.isSafeInteger(limit) || limit <= 0) throw new ConfigError(`invalid ${name}: ${limit}`)\n return limit\n}\n\nexport interface ResolvedSessionLimits {\n perConnection: number\n global: number\n}\n\n/**\n * Everything a function of `config` alone, resolved once at startup. Nothing\n * here touches a live ssh2 connection.\n */\nexport interface ResolvedRuntime {\n /** Host-key PEM(s) handed to the ssh2 `Server`. */\n hostKeys: (string | Buffer)[]\n /** SHA256 fingerprints of every configured host key. */\n fingerprints: string[]\n /** The security core, normalized off `auth` (+ any static allowlist). */\n authenticator: Authenticator\n /** Idle reap budget in ms, or undefined when no `idleTimeout` was set. */\n idleTimeoutMs: number | undefined\n /** Absolute session lifetime in ms, or undefined when no `maxTimeout` was set. */\n maxTimeoutMs: number | undefined\n /** Hard bounds for concurrently retained renderer-backed shells. */\n sessionLimits: ResolvedSessionLimits\n /** The error sink, closed over `onError`. */\n safe: SafeInvoke\n /** True when `none` is the only advertised method — listen() warns outside localhost. */\n noneOnly: boolean\n /** Data the startup banner is rendered from; formatted by `formatBanner` (banner.ts). */\n banner: BannerDescriptor\n}\n\n/**\n * Resolve a `ServerConfig` into the runtime the server runs on. Reads config and\n * the filesystem (for host keys); does not touch ssh2 connections. Throws when\n * the config admits no one (empty credentials).\n */\nexport function resolveRuntime(config: ServerConfig<AuthConfig>): ResolvedRuntime {\n const { hostKeyPems, fingerprints, algorithms, source } = resolveHostKey(config)\n const idleTimeoutMs = config.idleTimeout != null ? parseDuration(\"idleTimeout\", config.idleTimeout) : undefined\n const maxTimeoutMs = config.maxTimeout != null ? parseDuration(\"maxTimeout\", config.maxTimeout) : undefined\n const sessionLimits: ResolvedSessionLimits = {\n perConnection: parseLimit(\n \"limits.session.perConnection\",\n config.limits?.session?.perConnection,\n DEFAULT_SESSION_LIMITS.perConnection,\n ),\n global: parseLimit(\"limits.session.global\", config.limits?.session?.global, DEFAULT_SESSION_LIMITS.global),\n }\n\n // One error sink for handler, callback, connection, and server errors.\n const onError = config.onError ?? ((err: unknown) => console.error(err))\n // Keep user callback failures from escaping into ssh2 event handlers.\n const safe = createSafeInvoke(onError)\n\n // Auth failures from user predicates are reported through the same sink.\n const { authenticator, noneOnly, authorizedKeys } = resolveAuth(config.auth, safe.report)\n\n const banner: BannerDescriptor = { algorithms, source, methods: authenticator.advertisedMethods(), authorizedKeys }\n\n return {\n hostKeys: hostKeyPems,\n fingerprints,\n authenticator,\n idleTimeoutMs,\n maxTimeoutMs,\n sessionLimits,\n safe,\n noneOnly,\n banner,\n }\n}\n"
|
|
16
|
+
],
|
|
17
|
+
"mappings": "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",
|
|
18
|
+
"debugId": "AE7EAB0B30F431B864756E2164756E21",
|
|
19
|
+
"names": []
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opentui/ssh",
|
|
3
|
+
"module": "index.js",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"types": "src/index.d.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"version": "0.0.0-20260612-c3be6d8c",
|
|
8
|
+
"description": "Serve OpenTUI apps over SSH",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/anomalyco/opentui",
|
|
13
|
+
"directory": "packages/ssh"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./src/index.d.ts",
|
|
18
|
+
"import": "./index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"ssh2": "^1.16.0"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@opentui/core": "0.0.0-20260612-c3be6d8c"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"bun": ">=1.3.0",
|
|
29
|
+
"node": "26.3.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/auth.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { AuthContext } from "ssh2";
|
|
2
|
+
import type { AuthConfig, CredentialMethods, Identity, KeyboardPrompt, PublicKey } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Internal normalized policy the `Authenticator` decides over. `"open"` is
|
|
5
|
+
* normalized to `{ none: true }`; an `AuthMethods` set passes through.
|
|
6
|
+
*/
|
|
7
|
+
export interface NormalizedAuthConfig extends CredentialMethods {
|
|
8
|
+
/** Set by normalization when `auth` is `"open"`; never written by a user. */
|
|
9
|
+
none?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** The SSH auth methods this package understands. Subset of ssh2's `AuthenticationType`. */
|
|
12
|
+
export type AuthMethod = "none" | "password" | "publickey" | "keyboard-interactive";
|
|
13
|
+
/**
|
|
14
|
+
* A single auth attempt, narrowed to the fields a decision needs. This keeps the
|
|
15
|
+
* auth core testable without a live ssh2 handshake.
|
|
16
|
+
*/
|
|
17
|
+
export type AuthAttempt = {
|
|
18
|
+
method: "none";
|
|
19
|
+
username: string;
|
|
20
|
+
} | {
|
|
21
|
+
method: "password";
|
|
22
|
+
username: string;
|
|
23
|
+
password: string;
|
|
24
|
+
} | {
|
|
25
|
+
method: "keyboard-interactive";
|
|
26
|
+
username: string;
|
|
27
|
+
prompt: KeyboardPrompt;
|
|
28
|
+
} | {
|
|
29
|
+
method: "publickey";
|
|
30
|
+
username: string;
|
|
31
|
+
key: PublicKey;
|
|
32
|
+
/** Present only on the signed (second) pass; absent on the probe. */
|
|
33
|
+
signature?: Buffer;
|
|
34
|
+
/** The signed data; present alongside `signature`. */
|
|
35
|
+
blob?: Buffer;
|
|
36
|
+
hashAlgo?: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The verdict for one attempt. `acceptProbe` is the publickey query reply
|
|
40
|
+
* (PK_OK): the key is acceptable, so the client should sign — no identity yet.
|
|
41
|
+
* Only a verified attempt yields an `accept` with an `identity`.
|
|
42
|
+
*/
|
|
43
|
+
export type AuthOutcome = {
|
|
44
|
+
type: "accept";
|
|
45
|
+
identity: Identity;
|
|
46
|
+
} | {
|
|
47
|
+
type: "acceptProbe";
|
|
48
|
+
} | {
|
|
49
|
+
type: "reject";
|
|
50
|
+
methods: AuthMethod[];
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Adapt ssh2's live `AuthContext` into a plain `AuthAttempt`. Read-only w.r.t.
|
|
54
|
+
* ssh2 (never calls `ctx.accept()`/`reject()`), so the wiring is unit-testable
|
|
55
|
+
* without a real handshake. Returns `null` for an unmodeled method so `handle()`
|
|
56
|
+
* can reject it.
|
|
57
|
+
*/
|
|
58
|
+
export declare function attemptFromAuthContext(ctx: AuthContext): AuthAttempt | null;
|
|
59
|
+
export interface Authenticator {
|
|
60
|
+
/**
|
|
61
|
+
* Decide a live ssh2 auth context end to end: adapt, decide, and fail closed on
|
|
62
|
+
* any unexpected throw. The connection handler just applies the verdict.
|
|
63
|
+
*/
|
|
64
|
+
handle(ctx: AuthContext): Promise<AuthOutcome>;
|
|
65
|
+
/**
|
|
66
|
+
* Decide a single already-adapted attempt. Value-in, value-out — the unit seam
|
|
67
|
+
* for the security-critical paths (above all signature verification).
|
|
68
|
+
*/
|
|
69
|
+
authenticate(attempt: AuthAttempt): Promise<AuthOutcome>;
|
|
70
|
+
/** The configured methods, told to clients on reject and in the banner. */
|
|
71
|
+
advertisedMethods(): AuthMethod[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Build the auth decision core from normalized config and a pre-parsed allowlist.
|
|
75
|
+
*
|
|
76
|
+
* User predicates and signature verification run against client-supplied input.
|
|
77
|
+
* Any throw is reported through `onError` and treated as a reject.
|
|
78
|
+
*/
|
|
79
|
+
export declare function createAuthenticator(auth: NormalizedAuthConfig, authorizedKeys?: Set<string>, onError?: (err: unknown) => void): Authenticator;
|
|
80
|
+
/** The auth facts `resolveAuth` decides off the public `AuthConfig`. */
|
|
81
|
+
export interface ResolvedAuth {
|
|
82
|
+
/** The security core, normalized off `auth` (+ any static allowlist). */
|
|
83
|
+
authenticator: Authenticator;
|
|
84
|
+
/** True when `none` is the only advertised method — a wide-open server (listen() warns outside localhost). */
|
|
85
|
+
noneOnly: boolean;
|
|
86
|
+
/** Parsed static allowlist, surfaced for the startup banner; undefined when none. */
|
|
87
|
+
authorizedKeys: Set<string> | undefined;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the public `AuthConfig` into the running auth: normalize "open" to the
|
|
91
|
+
* internal none-only config, parse the static allowlist once at startup, build the
|
|
92
|
+
* decision core, and reject an empty credential set at startup.
|
|
93
|
+
*
|
|
94
|
+
* `onError` is the fail-closed sink threaded into the core (a throwing user
|
|
95
|
+
* predicate is denied + reported, never leaked) and into `handle()`.
|
|
96
|
+
*/
|
|
97
|
+
export declare function resolveAuth(auth: AuthConfig | undefined, onError: (err: unknown) => void): ResolvedAuth;
|
package/src/banner.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ListenInfo } from "./types.js";
|
|
2
|
+
/** Data a startup banner is rendered from; a function of config alone. */
|
|
3
|
+
export interface BannerDescriptor {
|
|
4
|
+
/** Host-key algorithms in fingerprint order. */
|
|
5
|
+
algorithms: string[];
|
|
6
|
+
/** Where the host key came from: "provided" / "ephemeral" / "loaded …" / "generated …". */
|
|
7
|
+
source: string;
|
|
8
|
+
/** Advertised auth methods, in banner order. */
|
|
9
|
+
methods: string[];
|
|
10
|
+
/** Base64 public-SSH blobs of the static allowlist; sampled for display. */
|
|
11
|
+
authorizedKeys?: Set<string>;
|
|
12
|
+
}
|
|
13
|
+
/** Startup-summary lines for `listen()`: pure formatting over a {@link BannerDescriptor} and bind. */
|
|
14
|
+
export declare function formatBanner(info: ListenInfo, descriptor: BannerDescriptor): string[];
|
package/src/bridge.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createCliRenderer, type CliRenderer } from "@opentui/core";
|
|
2
|
+
import type { ServerChannel } from "ssh2";
|
|
3
|
+
import { type SafeInvoke } from "./safe.js";
|
|
4
|
+
import type { Identity, MiddlewareSession, RemoteAddress, Session, SessionHandler } from "./types.js";
|
|
5
|
+
/** Renderer factory; injectable for renderer creation and disconnect-race tests. */
|
|
6
|
+
export type RendererFactory = (options: Parameters<typeof createCliRenderer>[0]) => Promise<CliRenderer>;
|
|
7
|
+
/** PTY parameters from the client's `pty-req`; the renderer sizes off cols/rows. */
|
|
8
|
+
export interface PtyInfo {
|
|
9
|
+
term: string;
|
|
10
|
+
cols: number;
|
|
11
|
+
rows: number;
|
|
12
|
+
hasPty: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare const DEFAULT_PTY: PtyInfo;
|
|
15
|
+
export declare const MAX_PTY: {
|
|
16
|
+
readonly cols: 500;
|
|
17
|
+
readonly rows: 200;
|
|
18
|
+
};
|
|
19
|
+
export interface SessionBridge {
|
|
20
|
+
/** One runtime object exposed through middleware and handler session views. */
|
|
21
|
+
session: Session & MiddlewareSession;
|
|
22
|
+
/** True once the session has closed (disconnect, end(), deny(), or idle reap). */
|
|
23
|
+
readonly closed: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Attach the renderer after middleware authorizes, run the handler, and resolve
|
|
26
|
+
* when the session closes.
|
|
27
|
+
*/
|
|
28
|
+
enterApp(handler: SessionHandler): Promise<void>;
|
|
29
|
+
resize(cols: number, rows: number): void;
|
|
30
|
+
destroy(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
/** What `createSessionBridge` needs to wire one ssh2 shell channel into a session. */
|
|
33
|
+
export interface SessionBridgeOptions {
|
|
34
|
+
pty: PtyInfo;
|
|
35
|
+
identity: Identity;
|
|
36
|
+
idleTimeoutMs: number | undefined;
|
|
37
|
+
maxTimeoutMs: number | undefined;
|
|
38
|
+
safe: SafeInvoke;
|
|
39
|
+
/** Injectable renderer factory; defaults to `createCliRenderer` (tests drive the race/failure paths). */
|
|
40
|
+
createRenderer?: RendererFactory;
|
|
41
|
+
remoteAddress?: RemoteAddress;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Turn an ssh2 shell channel into a wired-up OpenTUI session.
|
|
45
|
+
*
|
|
46
|
+
* The session starts without a renderer; `enterApp()` creates it only after the
|
|
47
|
+
* middleware chain reaches the handler. The throwing getter catches JS callers and
|
|
48
|
+
* unsafe casts that touch `session.renderer` too early.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createSessionBridge(channel: ServerChannel, options: SessionBridgeOptions): SessionBridge;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ClientInfo, Connection } from "ssh2";
|
|
2
|
+
import type { Authenticator } from "./auth.js";
|
|
3
|
+
import { type RuntimeMiddleware } from "./run-session.js";
|
|
4
|
+
import { type SafeInvoke } from "./safe.js";
|
|
5
|
+
import type { SessionHandler } from "./types.js";
|
|
6
|
+
import type { ResolvedSessionLimits } from "./runtime.js";
|
|
7
|
+
/** What the connection handler needs from the resolved runtime and sealed chain. */
|
|
8
|
+
export interface ConnectionDependencies {
|
|
9
|
+
authenticator: Authenticator;
|
|
10
|
+
middlewares: RuntimeMiddleware[];
|
|
11
|
+
handler: SessionHandler;
|
|
12
|
+
safe: SafeInvoke;
|
|
13
|
+
idleTimeoutMs: number | undefined;
|
|
14
|
+
maxTimeoutMs: number | undefined;
|
|
15
|
+
sessionLimits: ResolvedSessionLimits;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The per-connection ssh2 authentication → session → shell lifecycle.
|
|
19
|
+
* `onConnection` is the ssh2 `Server` connection listener; `closeAll` drains
|
|
20
|
+
* tracked bridges then sockets for `Server.close()`. Bridge/client tracking
|
|
21
|
+
* lives here because this is what spawns the sessions.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createConnectionHandler(dependencies: ConnectionDependencies): {
|
|
24
|
+
onConnection: (client: Connection, info: ClientInfo) => void;
|
|
25
|
+
closeAll: () => Promise<void>;
|
|
26
|
+
setAccepting: (accepting: boolean) => void;
|
|
27
|
+
};
|
package/src/errors.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package's error vocabulary: {@link SshError} for failures and
|
|
3
|
+
* {@link DenyError} for intentional session denial.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Base for every failure this package throws. Carries a stable `code` so a
|
|
7
|
+
* caller can branch without string-matching the message; `name` is the concrete
|
|
8
|
+
* subclass (e.g. `"ConfigError"`).
|
|
9
|
+
*/
|
|
10
|
+
export declare class SshError extends Error {
|
|
11
|
+
/** Stable, machine-branchable category — e.g. `"CONFIG"`. */
|
|
12
|
+
readonly code: string;
|
|
13
|
+
constructor(code: string, message: string);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A misconfiguration the developer must fix before the server can run (empty
|
|
17
|
+
* credentials, an unparseable host key, a malformed `idleTimeout`). Thrown at
|
|
18
|
+
* startup, never per-connection, so it surfaces when you wire the server up
|
|
19
|
+
* rather than on a client's first connect.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ConfigError extends SshError {
|
|
22
|
+
constructor(message: string);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The control-flow signal a middleware's `session.deny()` throws to unwind the
|
|
26
|
+
* chain — not a failure. `runSession` swallows it; anything that is not a
|
|
27
|
+
* `DenyError` routes to `onError`.
|
|
28
|
+
*/
|
|
29
|
+
export declare class DenyError extends Error {
|
|
30
|
+
/** The reason passed to `deny()`, if any — already delivered to the client. */
|
|
31
|
+
readonly reason: string | undefined;
|
|
32
|
+
constructor(reason?: string);
|
|
33
|
+
}
|
|
34
|
+
/** True for the deny control-flow signal — the one throw `runSession` swallows. */
|
|
35
|
+
export declare const isDeny: (err: unknown) => err is DenyError;
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { ConfigError, DenyError, SshError } from "./errors.js";
|
|
2
|
+
export { type LogEvent, type LoggingOptions, logging } from "./logging.js";
|
|
3
|
+
export { createServer } from "./server.js";
|
|
4
|
+
export type { AuthConfig, AuthMethods, Handoff, Identity, IdentityFor, ListenInfo, Middleware, MiddlewareFunction, MiddlewareSession, Next, PublicKey, PublicKeyPolicy, RemoteAddress, Session, SessionHandler, Server, ServerBuilder, ServerConfig, } from "./types.js";
|
package/src/keys.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ParsedKey } from "ssh2";
|
|
2
|
+
import type { ServerConfig } from "./types.js";
|
|
3
|
+
/** OpenSSH-style SHA256 fingerprint of a raw public-key blob (`ssh-keygen -lf` form, e.g. `SHA256:nThbg6kX…`). */
|
|
4
|
+
export declare function sha256Fingerprint(publicKeyBlob: Buffer): string;
|
|
5
|
+
/**
|
|
6
|
+
* Parse a single SSH key from any form `utils.parseKey` accepts (PEM,
|
|
7
|
+
* `authorized_keys` line, PPK), returning `null` on a parse error so callers
|
|
8
|
+
* choose their own failure. parseKey returns an array for multi-key inputs;
|
|
9
|
+
* this narrows to the first key.
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseOneKey(input: string | Buffer): ParsedKey | null;
|
|
12
|
+
/** Resolve host-key PEM(s) + fingerprints: explicit PEM, persisted path, or ephemeral. */
|
|
13
|
+
export declare function resolveHostKey(config: Pick<ServerConfig, "hostKey">): {
|
|
14
|
+
hostKeyPems: (string | Buffer)[];
|
|
15
|
+
fingerprints: string[];
|
|
16
|
+
algorithms: string[];
|
|
17
|
+
source: string;
|
|
18
|
+
};
|
package/src/logging.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Identity, Middleware, RemoteAddress } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* A connection-lifecycle event. Pure observability — the logging middleware
|
|
4
|
+
* never reports errors (those flow to `onError`, the one error sink); it only
|
|
5
|
+
* marks a session starting and ending.
|
|
6
|
+
*/
|
|
7
|
+
interface LogEventCommon<Id extends Identity> {
|
|
8
|
+
/** Who connected, narrowed to the server's configured auth. */
|
|
9
|
+
identity: Id;
|
|
10
|
+
remoteAddress: RemoteAddress;
|
|
11
|
+
term: string;
|
|
12
|
+
cols: number;
|
|
13
|
+
rows: number;
|
|
14
|
+
}
|
|
15
|
+
export type LogEvent<Id extends Identity = Identity> = (LogEventCommon<Id> & {
|
|
16
|
+
type: "connect";
|
|
17
|
+
durationMs?: never;
|
|
18
|
+
}) | (LogEventCommon<Id> & {
|
|
19
|
+
type: "disconnect";
|
|
20
|
+
durationMs: number;
|
|
21
|
+
});
|
|
22
|
+
export interface LoggingOptions<Id extends Identity = Identity> {
|
|
23
|
+
/** Sink for events. Defaults to a one-line `console.log` formatter. */
|
|
24
|
+
log?: (event: LogEvent<Id>) => void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Lifecycle logging as a `.use(...)` middleware: a "connect" event on entry, a
|
|
28
|
+
* "disconnect" (with duration) on teardown. It is a setup/teardown onion — the
|
|
29
|
+
* `finally` runs even when a downstream gate denies or the handler throws, so
|
|
30
|
+
* every session is logged — but it only ever returns the handoff, never swallows
|
|
31
|
+
* the throw. Errors stay the job of `onError`; this is observability alone.
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* createServer({ auth: { publicKey: "any" } })
|
|
35
|
+
* .use(logging()) // default: one line per event to console.log
|
|
36
|
+
* .serve((s) => mountApp(s.renderer))
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function logging<Id extends Identity = Identity>(options?: LoggingOptions<Id>): Middleware<Id>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SessionBridge } from "./bridge.js";
|
|
2
|
+
import type { SafeInvoke } from "./safe.js";
|
|
3
|
+
import type { Identity, MiddlewareSession, Next, SessionHandler } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Erased middleware-chain type for the runtime. Type safety lives in the public
|
|
6
|
+
* `ServerBuilder` interface; the impl runs with the contribution generic erased.
|
|
7
|
+
*/
|
|
8
|
+
export type RuntimeMiddleware<Id extends Identity = Identity> = (session: MiddlewareSession<Id>, next: Next) => unknown | Promise<unknown>;
|
|
9
|
+
/**
|
|
10
|
+
* Run the middleware onion around the handler for one session, then ensure the
|
|
11
|
+
* session is closed once the chain settles.
|
|
12
|
+
*
|
|
13
|
+
* `dispatch()` invokes link `i`; the link continues by calling `next()`, which
|
|
14
|
+
* recurses to link `i + 1`. `next(add)` merges `add` into the live per-session
|
|
15
|
+
* context bag BEFORE advancing, so every downstream link and the handler read it.
|
|
16
|
+
* The innermost `next()` reaches the leaf `bridge.enterApp(handler)`, which runs
|
|
17
|
+
* the handler and resolves at teardown — so a link's post-`next()` code runs as
|
|
18
|
+
* teardown. First `.use()` is the OUTERMOST link (use order === execution order).
|
|
19
|
+
*
|
|
20
|
+
* Settling means the session is over: normally at disconnect, or early when a link
|
|
21
|
+
* calls `deny()` (swallowed here) or never calls `next()`. The `finally` closes the
|
|
22
|
+
* session either way; `end()` is idempotent. A real (non-deny) throw reaches
|
|
23
|
+
* `safe()` → `onError`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function runSession(middlewares: RuntimeMiddleware[], handler: SessionHandler, bridge: SessionBridge, safe: SafeInvoke): void;
|
package/src/runtime.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type Authenticator } from "./auth.js";
|
|
2
|
+
import type { BannerDescriptor } from "./banner.js";
|
|
3
|
+
import { type SafeInvoke } from "./safe.js";
|
|
4
|
+
import type { AuthConfig, ServerConfig } from "./types.js";
|
|
5
|
+
export interface ResolvedSessionLimits {
|
|
6
|
+
perConnection: number;
|
|
7
|
+
global: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Everything a function of `config` alone, resolved once at startup. Nothing
|
|
11
|
+
* here touches a live ssh2 connection.
|
|
12
|
+
*/
|
|
13
|
+
export interface ResolvedRuntime {
|
|
14
|
+
/** Host-key PEM(s) handed to the ssh2 `Server`. */
|
|
15
|
+
hostKeys: (string | Buffer)[];
|
|
16
|
+
/** SHA256 fingerprints of every configured host key. */
|
|
17
|
+
fingerprints: string[];
|
|
18
|
+
/** The security core, normalized off `auth` (+ any static allowlist). */
|
|
19
|
+
authenticator: Authenticator;
|
|
20
|
+
/** Idle reap budget in ms, or undefined when no `idleTimeout` was set. */
|
|
21
|
+
idleTimeoutMs: number | undefined;
|
|
22
|
+
/** Absolute session lifetime in ms, or undefined when no `maxTimeout` was set. */
|
|
23
|
+
maxTimeoutMs: number | undefined;
|
|
24
|
+
/** Hard bounds for concurrently retained renderer-backed shells. */
|
|
25
|
+
sessionLimits: ResolvedSessionLimits;
|
|
26
|
+
/** The error sink, closed over `onError`. */
|
|
27
|
+
safe: SafeInvoke;
|
|
28
|
+
/** True when `none` is the only advertised method — listen() warns outside localhost. */
|
|
29
|
+
noneOnly: boolean;
|
|
30
|
+
/** Data the startup banner is rendered from; formatted by `formatBanner` (banner.ts). */
|
|
31
|
+
banner: BannerDescriptor;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a `ServerConfig` into the runtime the server runs on. Reads config and
|
|
35
|
+
* the filesystem (for host keys); does not touch ssh2 connections. Throws when
|
|
36
|
+
* the config admits no one (empty credentials).
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveRuntime(config: ServerConfig<AuthConfig>): ResolvedRuntime;
|