@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/src/safe.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Isolation guard for user callbacks. Each runs synchronously inside an ssh2
3
+ * event handler, where an uncaught throw or rejection would reach ssh2's emitter
4
+ * and could drop the connection or the process, and would starve sibling
5
+ * callbacks in the same dispatch.
6
+ *
7
+ * `safe(fn)` runs a callback and routes any throw/rejection to the sink; its
8
+ * returned promise always resolves, so callers can await without their own guard.
9
+ * `safe.report(err)` sinks an error with no callback (connection/server errors).
10
+ * A throwing sink is contained too.
11
+ */
12
+ export interface SafeInvoke {
13
+ (fn: () => unknown): Promise<void>;
14
+ /** Report an error directly to the sink, without ever letting the sink throw escape. */
15
+ report(err: unknown): void;
16
+ }
17
+ export declare function createSafeInvoke(onError: (err: unknown) => void): SafeInvoke;
18
+ /**
19
+ * Best-effort teardown: run `fn` and swallow any throw. Distinct from `safe`:
20
+ * this ignores the error rather than routing it to `onError`, for cleanup paths
21
+ * (renderer/channel/socket destroy) where a failure is not worth reporting.
22
+ */
23
+ export declare function ignoreErrors(fn: () => void): void;
@@ -0,0 +1,32 @@
1
+ import type { AuthConfig, AuthMethods, IdentityFor, ServerBuilder, ServerConfig } from "./types.js";
2
+ /**
3
+ * Create a server builder. Configure auth/host-key/etc. in the config object,
4
+ * then chain `.use(mw)` for each cross-cutting concern and seal with
5
+ * `serve(handler)`. `serve` takes the handler so the builder can flow the typed
6
+ * `context` the chain accumulates (via `next({ ... })`) into it; the builder has
7
+ * no `listen`, so a missing handler is a compile error.
8
+ *
9
+ * Inline middleware need no type arguments: `identity`/`context` flow from the
10
+ * builder, the contribution is inferred from `next({ ... })`. Author a reusable one
11
+ * by typing it as {@link Middleware} (or {@link MiddlewareFunction} when it reads
12
+ * upstream context). `.use(...)` order is execution order, first is OUTERMOST.
13
+ * `session.context` is the accumulation of every link's contribution.
14
+ *
15
+ * ```ts
16
+ * const server = createServer({ auth: { publicKey: "any" } })
17
+ * .use(async (s, next) => {
18
+ * const user = await lookup(s.identity.fingerprint)
19
+ * if (!user) s.deny("unknown key")
20
+ * return next({ user })
21
+ * })
22
+ * .serve((s) => mountApp(s.renderer, s.context.user))
23
+ * await server.listen()
24
+ * ```
25
+ */
26
+ export declare function createServer(config?: Omit<ServerConfig, "auth"> & {
27
+ auth?: "open";
28
+ }): ServerBuilder<IdentityFor<"open">>;
29
+ export declare function createServer<A extends AuthMethods>(config: Omit<ServerConfig, "auth"> & {
30
+ auth: A;
31
+ }): ServerBuilder<IdentityFor<A>>;
32
+ export declare function createServer<A extends AuthConfig>(config: ServerConfig<A>): ServerBuilder<IdentityFor<A | "open">>;
package/src/types.d.ts ADDED
@@ -0,0 +1,294 @@
1
+ import type { CliRenderer } from "@opentui/core";
2
+ /** A client public key as surfaced by ssh2 during authentication. */
3
+ export type PublicKey = {
4
+ algorithm: string;
5
+ blob: Buffer;
6
+ };
7
+ /**
8
+ * Discriminated union on `method`; only `publickey` carries `fingerprint`/`publicKey`.
9
+ * {@link IdentityFor} narrows it to exactly the configured methods.
10
+ *
11
+ * Security note: `username` is client-supplied. For `publickey`, key ownership is
12
+ * verified but the name is not; authorize by `fingerprint` and pair names to keys
13
+ * in `auth.publicKey.allow` if you need that binding. For password and
14
+ * keyboard-interactive auth, the name is only as trustworthy as your predicate.
15
+ */
16
+ export type Identity = {
17
+ method: "none";
18
+ username: string;
19
+ } | {
20
+ method: "password";
21
+ username: string;
22
+ } | {
23
+ method: "keyboard-interactive";
24
+ username: string;
25
+ } | {
26
+ method: "publickey";
27
+ /** Client-supplied label; not bound to the key. */
28
+ username: string;
29
+ /** The verified principal: SHA256 fingerprint of the authenticated key. */
30
+ fingerprint: string;
31
+ publicKey: PublicKey;
32
+ };
33
+ export interface RemoteAddress {
34
+ readonly address: string;
35
+ readonly port?: number;
36
+ }
37
+ declare const HANDOFF: unique symbol;
38
+ /**
39
+ * Opaque token returned by `next()`. Middleware return it to prove control was
40
+ * handed onward; `Add` is the context contributed by `next(add)`.
41
+ */
42
+ export interface Handoff<Add extends object = object> {
43
+ readonly [HANDOFF]: Add;
44
+ }
45
+ /**
46
+ * Hand off to the rest of the chain. Call bare to continue, or pass an object to
47
+ * contribute typed context to downstream middleware and the handler. The promise
48
+ * resolves when the session ends, making `finally` blocks natural teardown points.
49
+ */
50
+ export interface Next {
51
+ (): Promise<Handoff>;
52
+ <Add extends object>(add: Add): Promise<Handoff<Add>>;
53
+ }
54
+ /** Fields every session exposes, whether held by a middleware or the handler. */
55
+ export interface SessionCommon<Id extends Identity = Identity> {
56
+ /** Who connected, and how — narrowed to the configured auth methods. */
57
+ readonly identity: Id;
58
+ /** Terminal type reported by the client (e.g. "xterm-256color"). */
59
+ readonly term: string;
60
+ /** Current terminal size; updates on resize. */
61
+ readonly cols: number;
62
+ readonly rows: number;
63
+ /** Whether the client requested a PTY; false for bare shell channels. */
64
+ readonly hasPty: boolean;
65
+ /** Client socket address for logging, rate limiting, and policy. */
66
+ readonly remoteAddress: RemoteAddress;
67
+ /** Fired when the client resizes; renderer is already resized for you. */
68
+ onResize(cb: (cols: number, rows: number) => void): () => void;
69
+ /**
70
+ * Fired when the client disconnects. The renderer is already destroyed; use this
71
+ * for app-owned teardown such as framework roots, timers, or counters.
72
+ */
73
+ onClose(cb: () => void): () => void;
74
+ /**
75
+ * Write raw bytes to the client terminal, bypassing frame diffing — the escape
76
+ * hatch for control the renderer doesn't model (OSC 52, title, bell). No-op once closed.
77
+ */
78
+ write(data: Buffer | string): void;
79
+ /** Force-close this session. */
80
+ end(): void;
81
+ }
82
+ /**
83
+ * The session shape middleware receives. The renderer is created only after the
84
+ * chain authorizes, so gating middleware can deny before the alternate screen is
85
+ * entered. Contribute context with `next({ ... })`; gate with `deny()`.
86
+ */
87
+ export interface MiddlewareSession<Id extends Identity = Identity, Ctx extends object = object> extends SessionCommon<Id> {
88
+ /** Typed contributions from earlier middleware; `{}` at the chain head. */
89
+ readonly context: Ctx;
90
+ /**
91
+ * Deny this session before the handler runs. A reason is written to the main
92
+ * screen, the session closes, and the middleware chain unwinds as intended.
93
+ */
94
+ deny(reason?: string): never;
95
+ }
96
+ /**
97
+ * A reusable middleware: annotate a function with this type to author one outside
98
+ * a `.use(...)` call. `Add` is the context it contributes via `next({ ... })`; it
99
+ * sees the wide `Identity` and an untyped upstream `context` (a standalone link
100
+ * can't know what precedes it — use {@link MiddlewareFunction} to require a `Ctx`).
101
+ */
102
+ export type Middleware<Id extends Identity = Identity, Add extends object = object> = (session: MiddlewareSession<Id>, next: Next) => Handoff<Add> | Promise<Handoff<Add>>;
103
+ /**
104
+ * The call signature `.use(...)` accepts. `Ctx` is what earlier links contributed;
105
+ * `Add` is inferred from this link's `next({ ... })` call.
106
+ */
107
+ export type MiddlewareFunction<Id extends Identity = Identity, Ctx extends object = object, Add extends object = object> = (session: MiddlewareSession<Id, Ctx>, next: Next) => Handoff<Add> | Promise<Handoff<Add>>;
108
+ /**
109
+ * The per-session app handler. Receives a {@link Session} with the live renderer
110
+ * and combined middleware context. The renderer is destroyed on disconnect;
111
+ * `session.onClose` is for app-owned teardown.
112
+ */
113
+ export type SessionHandler<Id extends Identity = Identity, Ctx extends object = {}> = (session: Session<Id, Ctx>) => void | Promise<void>;
114
+ /**
115
+ * The session shape the handler receives: common fields plus the attached renderer
116
+ * and typed context.
117
+ */
118
+ export interface Session<Id extends Identity = Identity, Ctx extends object = {}> extends SessionCommon<Id> {
119
+ /** The sum of every middleware's contribution; `{}` with no middleware. Each session gets its own bag. */
120
+ readonly context: Ctx;
121
+ /**
122
+ * Pre-wired renderer: stdin/stdout = SSH channel, dims = client PTY. Use `write`
123
+ * for raw output the renderer doesn't model.
124
+ */
125
+ readonly renderer: CliRenderer;
126
+ }
127
+ /**
128
+ * Public-key admission policy. A key is admitted when it matches `authorizedKeys`,
129
+ * when `allow` returns true, or both.
130
+ */
131
+ export interface PublicKeyPolicy {
132
+ /** Static allowlist: a path to an authorized_keys file, or an array of public key strings. */
133
+ authorizedKeys?: string | string[];
134
+ /**
135
+ * Dynamic decision for checks a static file cannot express, such as DB lookup,
136
+ * revocation, or username-to-key pairing. Runs only after the signature verifies,
137
+ * so `fingerprint` and `publicKey` refer to a proven key.
138
+ */
139
+ allow?: (ctx: {
140
+ username: string;
141
+ fingerprint: string;
142
+ publicKey: PublicKey;
143
+ }) => boolean | Promise<boolean>;
144
+ }
145
+ /** Promise-based keyboard-interactive prompt, bridged onto ssh2's callback flow. */
146
+ export type KeyboardPrompt = (questions: {
147
+ prompt: string;
148
+ echo: boolean;
149
+ }[]) => Promise<string[]>;
150
+ /** The credential methods a client can present. Not re-exported from the package index. */
151
+ export interface CredentialMethods {
152
+ /**
153
+ * Public-key auth. `"any"` accepts & identifies any key; a `PublicKeyPolicy`
154
+ * configures an allowlist, an `allow` predicate, or both. `session.identity` is
155
+ * the publickey variant either way.
156
+ */
157
+ publicKey?: "any" | PublicKeyPolicy;
158
+ /** Password check. */
159
+ password?: (ctx: {
160
+ username: string;
161
+ password: string;
162
+ }) => boolean | Promise<boolean>;
163
+ /** Keyboard-interactive flow. */
164
+ keyboardInteractive?: (ctx: {
165
+ username: string;
166
+ prompt: KeyboardPrompt;
167
+ }) => boolean | Promise<boolean>;
168
+ }
169
+ /**
170
+ * The credential methods a client can present. Each is optional and they merge
171
+ * (the server advertises every one you set; the client picks). An empty set is
172
+ * rejected at startup; use `auth: "open"` for deliberate no-auth.
173
+ *
174
+ * `none` is typed `never` so a no-auth server and a credentialed one cannot be
175
+ * mixed: `{ none: true, publicKey }` is a compile error, not a silent open server.
176
+ */
177
+ export interface AuthMethods extends CredentialMethods {
178
+ /** Not a credential — use `auth: "open"` for no authentication. */
179
+ none?: never;
180
+ }
181
+ /**
182
+ * How clients authenticate: either `"open"` or a configured set of auth methods.
183
+ */
184
+ export type AuthConfig = "open" | AuthMethods;
185
+ /** Maps each configured credential key to its Identity variant. */
186
+ type CredentialVariant = {
187
+ password: Extract<Identity, {
188
+ method: "password";
189
+ }>;
190
+ keyboardInteractive: Extract<Identity, {
191
+ method: "keyboard-interactive";
192
+ }>;
193
+ publicKey: Extract<Identity, {
194
+ method: "publickey";
195
+ }>;
196
+ };
197
+ /** The credential keys actually configured in `A` (value is not `undefined`). */
198
+ type ConfiguredCredentialKeys<A extends AuthMethods> = {
199
+ [K in keyof A & keyof CredentialVariant]: A[K] extends undefined ? never : K;
200
+ }[keyof A & keyof CredentialVariant];
201
+ /**
202
+ * Narrows `Identity` to exactly the methods `A` configures: `"open"` → the `none`
203
+ * variant; an `AuthMethods` set → the union of its configured methods. You can only
204
+ * read a field you required.
205
+ */
206
+ export type IdentityFor<A extends AuthConfig = "open"> = A extends "open" ? Extract<Identity, {
207
+ method: "none";
208
+ }> : A extends AuthMethods ? CredentialVariant[ConfiguredCredentialKeys<A> & keyof CredentialVariant] : never;
209
+ /** Static server configuration, excluding the middleware chain and handler. */
210
+ export interface ServerConfig<A extends AuthConfig = "open"> {
211
+ /**
212
+ * How clients authenticate. Defaults to `"open"` (no auth) for localhost. Set a
213
+ * `AuthMethods` set instead: `{ publicKey: "any" }`, `{ publicKey: { authorizedKeys } }`, `{ password }`, …
214
+ * `session.identity` narrows to exactly the methods configured here. Listening
215
+ * on a host other than `localhost`, `127.0.0.1`, or `::1` while `"open"` warns
216
+ * (never throws); empty credentials throw.
217
+ */
218
+ auth?: A;
219
+ /** Host key source. If `path` is given and missing, it is generated & saved. */
220
+ hostKey?: {
221
+ path: string;
222
+ } | {
223
+ pem: string | Buffer | (string | Buffer)[];
224
+ };
225
+ /** Disconnect after this much inactivity, e.g. "10m" or ms. Optional. */
226
+ idleTimeout?: string | number;
227
+ /** Disconnect after this absolute session lifetime, e.g. "1h" or ms. Optional. */
228
+ maxTimeout?: string | number;
229
+ /** Resource limits for renderer-backed SSH shell sessions. */
230
+ limits?: {
231
+ session?: {
232
+ /** Maximum live shell sessions on one SSH connection. Default 1. */
233
+ perConnection?: number;
234
+ /** Maximum live shell sessions across this server. Default 100. */
235
+ global?: number;
236
+ };
237
+ };
238
+ /** Startup summary printed on listen(). Default true; set false to silence. */
239
+ startupBanner?: boolean;
240
+ /**
241
+ * The single runtime error sink — the *report* path. Contained application and
242
+ * transport errors land here: a throwing handler/middleware, a throwing
243
+ * `onResize`/`onClose`, a throwing auth predicate, and per-connection /
244
+ * server-level ssh2 errors. Logging sink failures are isolated and ignored so
245
+ * observability cannot affect a session.
246
+ * Defaults to `console.error`. A bind failure during `listen()` rejects the
247
+ * `listen()` promise instead of coming here.
248
+ *
249
+ * This is reporting, not reacting: to *react* to a session (deny, enrich, tear
250
+ * down, render an error screen) use middleware or `onClose`; to *observe* the
251
+ * connection lifecycle use the `logging` middleware.
252
+ */
253
+ onError?: (err: unknown) => void;
254
+ }
255
+ /** What `listen()` resolves to — useful for tests and programmatic callers. */
256
+ export interface ListenInfo {
257
+ host: string;
258
+ port: number;
259
+ /** SHA256 fingerprints for every configured host key, in configuration order. */
260
+ fingerprints: string[];
261
+ }
262
+ /**
263
+ * The builder returned by `createServer`. `.use(mw)` adds a link and accumulates
264
+ * its contribution into `Ctx`, so each subsequent `.use` and the handler see a
265
+ * `context` typed as the sum of every upstream link. No `listen` here — you must
266
+ * `serve(handler)` first, so "forgot the handler" is a compile error. `.use(...)`
267
+ * order === execution order: the first middleware is the outermost link.
268
+ */
269
+ export interface ServerBuilder<Id extends Identity = Identity, Ctx extends object = {}> {
270
+ /**
271
+ * Add a middleware link. Pass an inline arrow (`Id`/`Ctx` flow from the builder,
272
+ * contribution inferred from `next({ ... })`) or a reusable function typed as
273
+ * {@link Middleware} / {@link MiddlewareFunction}. Returns a builder whose `Ctx`
274
+ * is widened by this link's contribution.
275
+ */
276
+ use<Add extends object>(mw: MiddlewareFunction<Id, Ctx, Add>): ServerBuilder<Id, Ctx & Add>;
277
+ /**
278
+ * Seal the chain with the handler and return the startable server. The handler's
279
+ * `session.context` is the accumulated `Ctx`.
280
+ */
281
+ serve(handler: SessionHandler<Id, Ctx>): Server;
282
+ }
283
+ export interface Server {
284
+ /**
285
+ * Bind and start accepting (and, unless silenced, print the startup banner).
286
+ * Defaults to `2222` on `127.0.0.1` — pass `0` for an ephemeral port. Listening
287
+ * on a host other than `localhost`, `127.0.0.1`, or `::1` with no auth logs a
288
+ * warning (never throws).
289
+ */
290
+ listen(port?: number, host?: string): Promise<ListenInfo>;
291
+ /** Stop accepting, destroy live renderers, close the listener. */
292
+ close(): Promise<void>;
293
+ }
294
+ export {};