feat: add observability and CLI enhancements
Some checks failed
CI / Lint & Format (push) Failing after 16s
CI / Tests (push) Has been skipped
CI / Security Scan (push) Failing after 20s

Audit logging:
- audit_log table with event tracking
- app/audit.py module with log_event(), query_audit_log()
- GET /audit endpoint (admin only)
- configurable retention and cleanup

Prometheus metrics:
- app/metrics.py with custom counters
- paste create/access/delete, rate limit, PoW, dedup metrics
- instrumentation in API routes

CLI clipboard integration:
- fpaste create -C/--clipboard (read from clipboard)
- fpaste create --copy-url (copy result URL)
- fpaste get -c/--copy (copy content)
- cross-platform: xclip, xsel, pbcopy, wl-copy

Shell completions:
- completions/ directory with bash/zsh/fish scripts
- fpaste completion --shell command
This commit is contained in:
Username
2025-12-23 22:39:50 +01:00
parent 4d08a4467d
commit 7063f8718e
13 changed files with 2003 additions and 47 deletions

202
completions/fpaste.bash Normal file
View File

@@ -0,0 +1,202 @@
# Bash completion for fpaste
# Install: source this file or copy to /etc/bash_completion.d/fpaste
_fpaste_completions() {
local cur prev words cword
_init_completion || return
local commands="create c new get g delete d rm info i list ls search s find update u export register cert pki completion"
local pki_commands="status issue download dl"
# Handle command-level completion
if [[ $cword -eq 1 ]]; then
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
return
fi
local cmd="${words[1]}"
# PKI subcommand completion
if [[ "$cmd" == "pki" && $cword -eq 2 ]]; then
COMPREPLY=($(compgen -W "$pki_commands" -- "$cur"))
return
fi
# Option completion based on command
case "$cmd" in
create|c|new)
case "$prev" in
-x|--expiry)
# Suggest common expiry values
COMPREPLY=($(compgen -W "60 300 600 3600 86400 604800" -- "$cur"))
return
;;
-p|--password)
return
;;
esac
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "-E --no-encrypt -b --burn -x --expiry -p --password -r --raw -q --quiet -C --clipboard --copy-url" -- "$cur"))
else
_filedir
fi
;;
get|g)
case "$prev" in
-o|--output)
_filedir
return
;;
-p|--password)
return
;;
esac
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "-o --output -c --copy -p --password -m --meta" -- "$cur"))
fi
;;
delete|d|rm)
case "$prev" in
-c|--confirm)
return
;;
esac
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "-a --all -c --confirm" -- "$cur"))
fi
;;
info|i)
# No options
;;
list|ls)
case "$prev" in
-l|--limit|-o|--offset)
return
;;
esac
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "-a --all -l --limit -o --offset --json" -- "$cur"))
fi
;;
search|s|find)
case "$prev" in
-t|--type)
COMPREPLY=($(compgen -W "text/* image/* application/*" -- "$cur"))
return
;;
--after|--before)
return
;;
-l|--limit)
return
;;
esac
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "-t --type --after --before -l --limit --json" -- "$cur"))
fi
;;
update|u)
case "$prev" in
-x|--expiry)
COMPREPLY=($(compgen -W "60 300 600 3600 86400 604800" -- "$cur"))
return
;;
-p|--password)
return
;;
esac
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "-E --no-encrypt -p --password --remove-password -x --expiry -q --quiet" -- "$cur"))
else
_filedir
fi
;;
export)
case "$prev" in
-o|--output|-k|--keyfile)
_filedir
return
;;
esac
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "-o --output -k --keyfile --manifest -q --quiet" -- "$cur"))
fi
;;
register)
case "$prev" in
-n|--name|-o|--output)
return
;;
esac
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "-n --name -o --output --configure --p12-only -f --force -q --quiet" -- "$cur"))
fi
;;
cert)
case "$prev" in
-o|--output)
_filedir -d
return
;;
-a|--algorithm)
COMPREPLY=($(compgen -W "rsa ec" -- "$cur"))
return
;;
-c|--curve)
COMPREPLY=($(compgen -W "secp256r1 secp384r1 secp521r1" -- "$cur"))
return
;;
-b|--bits|-d|--days|-n|--name|--password-key)
return
;;
esac
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "-o --output -a --algorithm -b --bits -c --curve -d --days -n --name --password-key --configure -f --force" -- "$cur"))
fi
;;
pki)
local pki_cmd="${words[2]}"
case "$pki_cmd" in
issue)
case "$prev" in
-n|--name|-o|--output)
return
;;
esac
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "-n --name -o --output --configure -f --force" -- "$cur"))
fi
;;
download|dl)
case "$prev" in
-o|--output)
_filedir
return
;;
esac
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "-o --output --configure" -- "$cur"))
fi
;;
esac
;;
completion)
case "$prev" in
--shell)
COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur"))
return
;;
esac
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "--shell" -- "$cur"))
fi
;;
esac
# Global options
if [[ "$cur" == -* && $cword -eq 1 ]]; then
COMPREPLY=($(compgen -W "-s --server -h --help" -- "$cur"))
fi
}
complete -F _fpaste_completions fpaste

148
completions/fpaste.fish Normal file
View File

@@ -0,0 +1,148 @@
# Fish completion for fpaste
# Install: copy to ~/.config/fish/completions/fpaste.fish
# Disable file completions by default
complete -c fpaste -f
# Helper function to check if command is specified
function __fpaste_needs_command
set -l cmd (commandline -opc)
if test (count $cmd) -eq 1
return 0
end
return 1
end
function __fpaste_using_command
set -l cmd (commandline -opc)
if test (count $cmd) -gt 1
if test $argv[1] = $cmd[2]
return 0
end
end
return 1
end
function __fpaste_using_pki_command
set -l cmd (commandline -opc)
if test (count $cmd) -gt 2
if test $cmd[2] = 'pki' -a $argv[1] = $cmd[3]
return 0
end
end
return 1
end
# Global options
complete -c fpaste -s s -l server -d 'Server URL'
complete -c fpaste -s h -l help -d 'Show help'
# Commands
complete -c fpaste -n '__fpaste_needs_command' -a 'create' -d 'Create a new paste'
complete -c fpaste -n '__fpaste_needs_command' -a 'c' -d 'Create a new paste'
complete -c fpaste -n '__fpaste_needs_command' -a 'new' -d 'Create a new paste'
complete -c fpaste -n '__fpaste_needs_command' -a 'get' -d 'Retrieve a paste'
complete -c fpaste -n '__fpaste_needs_command' -a 'g' -d 'Retrieve a paste'
complete -c fpaste -n '__fpaste_needs_command' -a 'delete' -d 'Delete paste(s)'
complete -c fpaste -n '__fpaste_needs_command' -a 'd' -d 'Delete paste(s)'
complete -c fpaste -n '__fpaste_needs_command' -a 'rm' -d 'Delete paste(s)'
complete -c fpaste -n '__fpaste_needs_command' -a 'info' -d 'Show server info'
complete -c fpaste -n '__fpaste_needs_command' -a 'i' -d 'Show server info'
complete -c fpaste -n '__fpaste_needs_command' -a 'list' -d 'List your pastes'
complete -c fpaste -n '__fpaste_needs_command' -a 'ls' -d 'List your pastes'
complete -c fpaste -n '__fpaste_needs_command' -a 'search' -d 'Search your pastes'
complete -c fpaste -n '__fpaste_needs_command' -a 's' -d 'Search your pastes'
complete -c fpaste -n '__fpaste_needs_command' -a 'find' -d 'Search your pastes'
complete -c fpaste -n '__fpaste_needs_command' -a 'update' -d 'Update existing paste'
complete -c fpaste -n '__fpaste_needs_command' -a 'u' -d 'Update existing paste'
complete -c fpaste -n '__fpaste_needs_command' -a 'export' -d 'Export all pastes'
complete -c fpaste -n '__fpaste_needs_command' -a 'register' -d 'Register and get certificate'
complete -c fpaste -n '__fpaste_needs_command' -a 'cert' -d 'Generate client certificate'
complete -c fpaste -n '__fpaste_needs_command' -a 'pki' -d 'PKI operations'
complete -c fpaste -n '__fpaste_needs_command' -a 'completion' -d 'Generate shell completion'
# create command options
complete -c fpaste -n '__fpaste_using_command create; or __fpaste_using_command c; or __fpaste_using_command new' -s E -l no-encrypt -d 'Disable encryption'
complete -c fpaste -n '__fpaste_using_command create; or __fpaste_using_command c; or __fpaste_using_command new' -s b -l burn -d 'Burn after read'
complete -c fpaste -n '__fpaste_using_command create; or __fpaste_using_command c; or __fpaste_using_command new' -s x -l expiry -d 'Expiry in seconds' -x
complete -c fpaste -n '__fpaste_using_command create; or __fpaste_using_command c; or __fpaste_using_command new' -s p -l password -d 'Password protect' -x
complete -c fpaste -n '__fpaste_using_command create; or __fpaste_using_command c; or __fpaste_using_command new' -s r -l raw -d 'Output raw URL'
complete -c fpaste -n '__fpaste_using_command create; or __fpaste_using_command c; or __fpaste_using_command new' -s q -l quiet -d 'Output ID only'
complete -c fpaste -n '__fpaste_using_command create; or __fpaste_using_command c; or __fpaste_using_command new' -s C -l clipboard -d 'Read from clipboard'
complete -c fpaste -n '__fpaste_using_command create; or __fpaste_using_command c; or __fpaste_using_command new' -l copy-url -d 'Copy result URL to clipboard'
complete -c fpaste -n '__fpaste_using_command create; or __fpaste_using_command c; or __fpaste_using_command new' -F
# get command options
complete -c fpaste -n '__fpaste_using_command get; or __fpaste_using_command g' -s o -l output -d 'Save to file' -r
complete -c fpaste -n '__fpaste_using_command get; or __fpaste_using_command g' -s c -l copy -d 'Copy content to clipboard'
complete -c fpaste -n '__fpaste_using_command get; or __fpaste_using_command g' -s p -l password -d 'Password' -x
complete -c fpaste -n '__fpaste_using_command get; or __fpaste_using_command g' -s m -l meta -d 'Show metadata only'
# delete command options
complete -c fpaste -n '__fpaste_using_command delete; or __fpaste_using_command d; or __fpaste_using_command rm' -s a -l all -d 'Delete all pastes'
complete -c fpaste -n '__fpaste_using_command delete; or __fpaste_using_command d; or __fpaste_using_command rm' -s c -l confirm -d 'Confirm count' -x
# list command options
complete -c fpaste -n '__fpaste_using_command list; or __fpaste_using_command ls' -s a -l all -d 'List all pastes (admin)'
complete -c fpaste -n '__fpaste_using_command list; or __fpaste_using_command ls' -s l -l limit -d 'Max pastes' -x
complete -c fpaste -n '__fpaste_using_command list; or __fpaste_using_command ls' -s o -l offset -d 'Skip first N pastes' -x
complete -c fpaste -n '__fpaste_using_command list; or __fpaste_using_command ls' -l json -d 'Output as JSON'
# search command options
complete -c fpaste -n '__fpaste_using_command search; or __fpaste_using_command s; or __fpaste_using_command find' -s t -l type -d 'Filter by MIME type' -x
complete -c fpaste -n '__fpaste_using_command search; or __fpaste_using_command s; or __fpaste_using_command find' -l after -d 'Created after' -x
complete -c fpaste -n '__fpaste_using_command search; or __fpaste_using_command s; or __fpaste_using_command find' -l before -d 'Created before' -x
complete -c fpaste -n '__fpaste_using_command search; or __fpaste_using_command s; or __fpaste_using_command find' -s l -l limit -d 'Max results' -x
complete -c fpaste -n '__fpaste_using_command search; or __fpaste_using_command s; or __fpaste_using_command find' -l json -d 'Output as JSON'
# update command options
complete -c fpaste -n '__fpaste_using_command update; or __fpaste_using_command u' -s E -l no-encrypt -d 'Disable encryption'
complete -c fpaste -n '__fpaste_using_command update; or __fpaste_using_command u' -s p -l password -d 'Set/change password' -x
complete -c fpaste -n '__fpaste_using_command update; or __fpaste_using_command u' -l remove-password -d 'Remove password'
complete -c fpaste -n '__fpaste_using_command update; or __fpaste_using_command u' -s x -l expiry -d 'Extend expiry' -x
complete -c fpaste -n '__fpaste_using_command update; or __fpaste_using_command u' -s q -l quiet -d 'Minimal output'
complete -c fpaste -n '__fpaste_using_command update; or __fpaste_using_command u' -F
# export command options
complete -c fpaste -n '__fpaste_using_command export' -s o -l output -d 'Output directory' -r
complete -c fpaste -n '__fpaste_using_command export' -s k -l keyfile -d 'Key file' -r
complete -c fpaste -n '__fpaste_using_command export' -l manifest -d 'Write manifest.json'
complete -c fpaste -n '__fpaste_using_command export' -s q -l quiet -d 'Minimal output'
# register command options
complete -c fpaste -n '__fpaste_using_command register' -s n -l name -d 'Common name' -x
complete -c fpaste -n '__fpaste_using_command register' -s o -l output -d 'Output directory' -r
complete -c fpaste -n '__fpaste_using_command register' -l configure -d 'Update config file'
complete -c fpaste -n '__fpaste_using_command register' -l p12-only -d 'Save only PKCS#12'
complete -c fpaste -n '__fpaste_using_command register' -s f -l force -d 'Overwrite existing files'
complete -c fpaste -n '__fpaste_using_command register' -s q -l quiet -d 'Minimal output'
# cert command options
complete -c fpaste -n '__fpaste_using_command cert' -s o -l output -d 'Output directory' -r
complete -c fpaste -n '__fpaste_using_command cert' -s a -l algorithm -d 'Key algorithm' -x -a 'rsa ec'
complete -c fpaste -n '__fpaste_using_command cert' -s b -l bits -d 'RSA key size' -x
complete -c fpaste -n '__fpaste_using_command cert' -s c -l curve -d 'EC curve' -x -a 'secp256r1 secp384r1 secp521r1'
complete -c fpaste -n '__fpaste_using_command cert' -s d -l days -d 'Validity period' -x
complete -c fpaste -n '__fpaste_using_command cert' -s n -l name -d 'Common name' -x
complete -c fpaste -n '__fpaste_using_command cert' -l password-key -d 'Encrypt private key' -x
complete -c fpaste -n '__fpaste_using_command cert' -l configure -d 'Update config file'
complete -c fpaste -n '__fpaste_using_command cert' -s f -l force -d 'Overwrite existing files'
# pki subcommands
complete -c fpaste -n '__fpaste_using_command pki' -a 'status' -d 'Show PKI status'
complete -c fpaste -n '__fpaste_using_command pki' -a 'issue' -d 'Request certificate from server'
complete -c fpaste -n '__fpaste_using_command pki' -a 'download' -d 'Download CA certificate'
complete -c fpaste -n '__fpaste_using_command pki' -a 'dl' -d 'Download CA certificate'
# pki issue options
complete -c fpaste -n '__fpaste_using_pki_command issue' -s n -l name -d 'Common name' -x
complete -c fpaste -n '__fpaste_using_pki_command issue' -s o -l output -d 'Output directory' -r
complete -c fpaste -n '__fpaste_using_pki_command issue' -l configure -d 'Update config file'
complete -c fpaste -n '__fpaste_using_pki_command issue' -s f -l force -d 'Overwrite existing files'
# pki download options
complete -c fpaste -n '__fpaste_using_pki_command download; or __fpaste_using_pki_command dl' -s o -l output -d 'Save to file' -r
complete -c fpaste -n '__fpaste_using_pki_command download; or __fpaste_using_pki_command dl' -l configure -d 'Update config file'
# completion command options
complete -c fpaste -n '__fpaste_using_command completion' -l shell -d 'Shell type' -x -a 'bash zsh fish'

203
completions/fpaste.zsh Normal file
View File

@@ -0,0 +1,203 @@
#compdef fpaste
# Zsh completion for fpaste
# Install: copy to ~/.zfunc/_fpaste and add 'fpath=(~/.zfunc $fpath)' to ~/.zshrc
_fpaste() {
local curcontext="$curcontext" state line
typeset -A opt_args
_arguments -C \
'-s[Server URL]:url:' \
'--server[Server URL]:url:' \
'-h[Show help]' \
'--help[Show help]' \
'1: :->command' \
'*:: :->args'
case $state in
command)
local commands=(
'create:Create a new paste'
'c:Create a new paste'
'new:Create a new paste'
'get:Retrieve a paste'
'g:Retrieve a paste'
'delete:Delete paste(s)'
'd:Delete paste(s)'
'rm:Delete paste(s)'
'info:Show server info'
'i:Show server info'
'list:List your pastes'
'ls:List your pastes'
'search:Search your pastes'
's:Search your pastes'
'find:Search your pastes'
'update:Update existing paste'
'u:Update existing paste'
'export:Export all pastes'
'register:Register and get certificate'
'cert:Generate client certificate'
'pki:PKI operations'
'completion:Generate shell completion'
)
_describe -t commands 'fpaste commands' commands
;;
args)
case $line[1] in
create|c|new)
_arguments \
'-E[Disable encryption]' \
'--no-encrypt[Disable encryption]' \
'-b[Burn after read]' \
'--burn[Burn after read]' \
'-x[Expiry in seconds]:seconds:' \
'--expiry[Expiry in seconds]:seconds:' \
'-p[Password protect]:password:' \
'--password[Password protect]:password:' \
'-r[Output raw URL]' \
'--raw[Output raw URL]' \
'-q[Output ID only]' \
'--quiet[Output ID only]' \
'-C[Read from clipboard]' \
'--clipboard[Read from clipboard]' \
'--copy-url[Copy result URL to clipboard]' \
'*:file:_files'
;;
get|g)
_arguments \
'-o[Save to file]:file:_files' \
'--output[Save to file]:file:_files' \
'-c[Copy content to clipboard]' \
'--copy[Copy content to clipboard]' \
'-p[Password]:password:' \
'--password[Password]:password:' \
'-m[Show metadata only]' \
'--meta[Show metadata only]' \
'1:paste ID:'
;;
delete|d|rm)
_arguments \
'-a[Delete all pastes]' \
'--all[Delete all pastes]' \
'-c[Confirm count]:count:' \
'--confirm[Confirm count]:count:' \
'*:paste ID:'
;;
info|i)
;;
list|ls)
_arguments \
'-a[List all pastes (admin)]' \
'--all[List all pastes (admin)]' \
'-l[Max pastes]:number:' \
'--limit[Max pastes]:number:' \
'-o[Skip first N pastes]:number:' \
'--offset[Skip first N pastes]:number:' \
'--json[Output as JSON]'
;;
search|s|find)
_arguments \
'-t[Filter by MIME type]:pattern:' \
'--type[Filter by MIME type]:pattern:' \
'--after[Created after]:date:' \
'--before[Created before]:date:' \
'-l[Max results]:number:' \
'--limit[Max results]:number:' \
'--json[Output as JSON]'
;;
update|u)
_arguments \
'-E[Disable encryption]' \
'--no-encrypt[Disable encryption]' \
'-p[Set/change password]:password:' \
'--password[Set/change password]:password:' \
'--remove-password[Remove password]' \
'-x[Extend expiry]:seconds:' \
'--expiry[Extend expiry]:seconds:' \
'-q[Minimal output]' \
'--quiet[Minimal output]' \
'1:paste ID:' \
'*:file:_files'
;;
export)
_arguments \
'-o[Output directory]:directory:_files -/' \
'--output[Output directory]:directory:_files -/' \
'-k[Key file]:file:_files' \
'--keyfile[Key file]:file:_files' \
'--manifest[Write manifest.json]' \
'-q[Minimal output]' \
'--quiet[Minimal output]'
;;
register)
_arguments \
'-n[Common name]:name:' \
'--name[Common name]:name:' \
'-o[Output directory]:directory:_files -/' \
'--output[Output directory]:directory:_files -/' \
'--configure[Update config file]' \
'--p12-only[Save only PKCS#12]' \
'-f[Overwrite existing files]' \
'--force[Overwrite existing files]' \
'-q[Minimal output]' \
'--quiet[Minimal output]'
;;
cert)
_arguments \
'-o[Output directory]:directory:_files -/' \
'--output[Output directory]:directory:_files -/' \
'-a[Key algorithm]:algorithm:(rsa ec)' \
'--algorithm[Key algorithm]:algorithm:(rsa ec)' \
'-b[RSA key size]:bits:' \
'--bits[RSA key size]:bits:' \
'-c[EC curve]:curve:(secp256r1 secp384r1 secp521r1)' \
'--curve[EC curve]:curve:(secp256r1 secp384r1 secp521r1)' \
'-d[Validity period]:days:' \
'--days[Validity period]:days:' \
'-n[Common name]:name:' \
'--name[Common name]:name:' \
'--password-key[Encrypt private key]:password:' \
'--configure[Update config file]' \
'-f[Overwrite existing files]' \
'--force[Overwrite existing files]'
;;
pki)
local pki_commands=(
'status:Show PKI status'
'issue:Request certificate from server'
'download:Download CA certificate'
'dl:Download CA certificate'
)
if (( CURRENT == 2 )); then
_describe -t commands 'pki commands' pki_commands
else
case $line[2] in
issue)
_arguments \
'-n[Common name]:name:' \
'--name[Common name]:name:' \
'-o[Output directory]:directory:_files -/' \
'--output[Output directory]:directory:_files -/' \
'--configure[Update config file]' \
'-f[Overwrite existing files]' \
'--force[Overwrite existing files]'
;;
download|dl)
_arguments \
'-o[Save to file]:file:_files' \
'--output[Save to file]:file:_files' \
'--configure[Update config file]'
;;
esac
fi
;;
completion)
_arguments \
'--shell[Shell type]:shell:(bash zsh fish)'
;;
esac
;;
esac
}
_fpaste "$@"