mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-04 00:47:01 +01:00
feat: mount spaces/folders over SFTP with SSH-key auth
Add an in-process SFTP server (russh) backed directly by the nodes table, so a space can be edited from nvim or synced with sshfs without a separate filesystem mirror. Access is authenticated by public key against a new ssh_keys table, managed via :sshkey in the app; the feature stays off until WORKSPACE_SFTP_BIND is set.
This commit is contained in:
1 parent
ab8712559e
commit
747332153c
12 files changed
+1950
-93
No files matched your search
@@ -30,6 +30,15 @@ export interface WorkspaceNode extends NodeSummary {
|
||||
path: Crumb[]
|
||||
}
|
||||
|
||||
/** A public key allowed to mount the workspace over SFTP. */
|
||||
export interface SshKey {
|
||||
id: number
|
||||
name: string
|
||||
fingerprint: string
|
||||
created_at: number
|
||||
last_used_at: number | null
|
||||
}
|
||||
|
||||
export interface SharedDocument {
|
||||
name: string
|
||||
body: string
|
||||
@@ -248,4 +257,11 @@ export const api = {
|
||||
|
||||
readShared: (token: string, key: string) =>
|
||||
request<SharedDocument>(`/share/${encodeURIComponent(token)}/${encodeURIComponent(key)}`),
|
||||
|
||||
listSshKeys: () => request<SshKey[]>('/ssh-keys'),
|
||||
|
||||
addSshKey: (name: string, public_key: string) =>
|
||||
request<SshKey>('/ssh-keys', { method: 'POST', body: body({ name, public_key }) }),
|
||||
|
||||
removeSshKey: (id: number) => request<{ ok: true }>(`/ssh-keys/${id}`, { method: 'DELETE' }),
|
||||
}
|
||||
@@ -703,6 +703,36 @@ export const commands: CommandSpec[] = [
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'sshkey',
|
||||
args: '[add <name> <key> | rm <name>]',
|
||||
group: 'view',
|
||||
help: 'list, add or remove keys allowed to mount the workspace over sftp',
|
||||
run: (arg) =>
|
||||
workspace.guard(async () => {
|
||||
const trimmed = arg.trim()
|
||||
if (trimmed === '' || trimmed === 'ls' || trimmed === 'list') {
|
||||
const keys = await api.listSshKeys()
|
||||
if (keys.length === 0) return workspace.say('no keys registered — :sshkey add <name> <key>')
|
||||
return workspace.say(keys.map((k) => `${k.name} ${k.fingerprint}`).join(' · '))
|
||||
}
|
||||
const addMatch = /^add\s+(\S+)\s+(.+)$/.exec(trimmed)
|
||||
if (addMatch) {
|
||||
const [, name, key] = addMatch
|
||||
const added = await api.addSshKey(name, key)
|
||||
return workspace.say(`added ${added.name} (${added.fingerprint})`)
|
||||
}
|
||||
const rmMatch = /^rm\s+(\S+)$/.exec(trimmed)
|
||||
if (rmMatch) {
|
||||
const keys = await api.listSshKeys()
|
||||
const target = keys.find((k) => k.name === rmMatch[1])
|
||||
if (!target) return workspace.say(`no key named ${rmMatch[1]}`, 'error')
|
||||
await api.removeSshKey(target.id)
|
||||
return workspace.say(`removed ${target.name}`)
|
||||
}
|
||||
workspace.say('usage: :sshkey [add <name> <key> | rm <name>]', 'error')
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'help',
|
||||
aliases: ['h'],
|
||||
|
||||
Reference in new issue
Block a user