import { StoreLocator, StoreDescriptor, StoreFactory, NoydbStore } from '@noy-db/hub/to'; /** * **@noy-db/to-ssh** — SSH/SFTP-backed noy-db store. * * Any Linux/macOS server with `sshd` running becomes a noy-db backend, * using the keys already in the operator's `~/.ssh/` or ssh-agent. * SFTP — not per-call SCP — keeps the overhead to a single long-lived * SSH channel regardless of how many records are read or written. * * ## Auth — keys only, never passwords * * Three paths, pick one per store instance: * * 1. **Private key bytes** — `privateKey: Buffer | string` (optional * `passphrase` for encrypted keys). * 2. **Private key file path** — `privateKeyPath: '~/.ssh/id_ed25519'`; * the store reads and decrypts it at connect time. * 3. **ssh-agent** — `agent: process.env.SSH_AUTH_SOCK` (default when * no other option is supplied). Leverages the existing keys without * handing them to noy-db at all. * * Password auth is intentionally **not supported**. A password on the * wire defeats the zero-knowledge positioning and offers worse UX than * a key. * * ## Driver — bring your own * * noy-db does not pull in `ssh2` as a runtime dependency. The consumer * installs it (`pnpm add ssh2`) and either passes a connected `Client` * directly (shared across adapters) or lets the factory connect one * for them. The duck-typed `SftpHandle` interface below accepts any * shape that exposes the minimal SFTP verbs we need, so wrappers like * `ssh2-sftp-client` work too. * * ## Atomicity * * Every put writes to `{id}.json.tmp` then issues `SFTP_RENAME` to * `{id}.json`. POSIX rename is atomic, so a concurrent reader cannot * observe a half-written record. This does NOT give CAS — * `StoreCapabilities.casAtomic` is `false` — but it rules out partial * writes on process crash. * * @packageDocumentation */ /** * Duck-typed subset of an SFTP client. Compatible with `ssh2`'s * `SFTPWrapper`, `ssh2-sftp-client`'s API, or any custom wrapper that * exposes the same async file primitives. */ interface SftpHandle { /** Read a file into memory. Returns `null` if the file does not exist. */ readFile(path: string): Promise; /** Write a file (create or overwrite). */ writeFile(path: string, data: Uint8Array | Buffer | string): Promise; /** Delete a file. Succeeds silently if the file does not exist. */ unlink(path: string): Promise; /** Create a directory and all missing parents. */ mkdir(path: string, recursive?: boolean): Promise; /** * Atomic rename. MUST be atomic against concurrent readers on the * same path (POSIX guarantee on same-filesystem renames). */ rename(from: string, to: string): Promise; /** List entries of a directory. Returns empty when directory is missing. */ readdir(path: string): Promise; /** Optional liveness check. When missing, the store's `ping` returns `true`. */ ping?(): Promise; } interface SshStoreOptions { /** Connected SFTP handle — consumer supplies this. */ readonly sftp: SftpHandle; /** Remote directory root. Created on first write if missing. Default `'noydb'`. */ readonly remotePath?: string; /** Diagnostic name. Default `'ssh'`. */ readonly name?: string; } declare function toSsh(options: SshStoreOptions): NoydbStore; /** * Serializable location of an SSH/SFTP store. `host` and `port` are * identity-only — the connection lives in the injected `binding.client`, * so the factory does not consume them. */ interface SshAddress { /** Identity-only: not consumed by the factory (the connection carries it). */ readonly host?: string; /** Identity-only: not consumed by the factory (the connection carries it). */ readonly port?: number; /** Maps to `SshStoreOptions.remotePath`. Default `'noydb'` when omitted. */ readonly path?: string; } /** Serializable tuning carried on the descriptor (never credentials). */ interface SshDescriptorOptions { readonly name?: string; } /** * Device-local supplement resolved at `resolve()` time — the live * `SftpHandle` this store has no way to construct itself. Never serialized * into a pod alongside the descriptor. */ interface SshBinding { readonly client: SftpHandle; } /** * Builds the `StoreDescriptor` form of a `toSsh()` store: * `kind: 'ssh'`, `class: 'lan'`, with the identity address and the * serializable tuning as `options`. Credentialless by construction — the * live connection arrives via `binding.client` at `resolve()` time. */ declare function sshStoreDescriptor(address: SshAddress, options?: SshDescriptorOptions): StoreDescriptor; /** * `StoreFactory` for `to-ssh`: reconstructs the same store `toSsh()` * builds, from a descriptor produced by {@link sshStoreDescriptor}. * `opts.binding.client` is required — this store has no client library of * its own and cannot build a connection from `address` alone. */ declare const sshStoreFactory: StoreFactory; /** Registers {@link sshStoreFactory} under the `'ssh'` kind on `locator`. */ declare function registerSshStore(locator: StoreLocator): void; export { type SftpHandle, type SshAddress, type SshBinding, type SshDescriptorOptions, type SshStoreOptions, registerSshStore, sshStoreDescriptor, sshStoreFactory, toSsh };