onetime-up 0.6.2 → 0.8.0
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.
- checksums.yaml +4 -4
- data/README.md +13 -6
- data/bin/onetime +2 -215
- data/lib/onetime/cli/parser.rb +134 -0
- data/lib/onetime/cli/runner.rb +210 -0
- data/lib/onetime/cli.rb +20 -0
- data/lib/onetime/version.rb +1 -1
- metadata +5 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4c1dc14a9677195dcc44ddb5f2f0ab15a310c2289a143d97aa2097d62e4f056f
|
|
4
|
+
data.tar.gz: ebb13a2e7050fffc000a18befa1d7ef456151a23f30d9dcc0d75b5ce084e20b6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 021a70dcfccc65e85f3510c02e620b351302275db33989571edb809af2956e359cbdfdf0254dd2128631c7c2751c2243b9663a97f1ee9443903cc6307ce252df
|
|
7
|
+
data.tar.gz: 44786b8cd1a188dd8fefa88766083da6fbb000de5d44465e66ed00832dc1334b3dab2e831b7bc065395a2a1fc138745fdeef0277d9f75353b78a7ca269b6cb16
|
data/README.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# Onetime-up
|
|
2
2
|
|
|
3
|
-
Fork of the Ruby program [`One-Time Secret`](https://github.com/onetimesecret/onetime-ruby), using the API v2
|
|
3
|
+
Fork of the Ruby program [`One-Time Secret`](https://github.com/onetimesecret/onetime-ruby), using the API v2, with the following improvements:
|
|
4
|
+
|
|
5
|
+
- CLI built on Ruby stdlib — no third-party CLI dependency (the upstream `drydock` requirement has been removed)
|
|
6
|
+
- Uses the API v2 (response shapes updated accordingly)
|
|
7
|
+
- `share` accepts a positional file path (e.g. `onetime share /path/to/file`) in addition to stdin redirection
|
|
8
|
+
- `generate` rejects misuse (extra positional args, piped stdin) with a clear hint pointing at `onetime share`
|
|
9
|
+
- Default API host is `https://eu.onetimesecret.com/api`
|
|
10
|
+
- The `metadata` command is renamed `receipt` for consistency with the API
|
|
4
11
|
|
|
5
12
|
## Basic usage
|
|
6
13
|
|
|
@@ -8,12 +15,10 @@ Share a secret:
|
|
|
8
15
|
|
|
9
16
|
```sh
|
|
10
17
|
echo "secret text" | onetime share
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Share a secret protected by a passphrase:
|
|
14
|
-
|
|
15
|
-
```sh
|
|
18
|
+
# Share a secret protected by a passphrase:
|
|
16
19
|
echo "secret text" | onetime share -p "shared-passphrase"
|
|
20
|
+
# Share the contents of a local file:
|
|
21
|
+
onetime share /path/to/file
|
|
17
22
|
```
|
|
18
23
|
|
|
19
24
|
Retrieve a secret:
|
|
@@ -42,3 +47,5 @@ The command `metadata` has been renamed `receipt` for consistency with the API.
|
|
|
42
47
|
The default API host is now `https://eu.onetimesecret.com/api`.
|
|
43
48
|
|
|
44
49
|
The library now uses API v2 response shapes. Programmatic callers should read receipt data from `record.receipt` and secret data from `record.secret` or reveal responses from `record.secret_value`.
|
|
50
|
+
|
|
51
|
+
The `generate` command now rejects extra positional arguments and piped stdin (previously both were silently ignored). Use `onetime share` to share file contents or piped data.
|
data/bin/onetime
CHANGED
|
@@ -3,219 +3,6 @@
|
|
|
3
3
|
base_path = File.expand_path File.join(File.dirname(__FILE__), '..')
|
|
4
4
|
$:.unshift File.join(base_path, 'lib')
|
|
5
5
|
|
|
6
|
-
require '
|
|
7
|
-
require 'onetime/api'
|
|
8
|
-
require 'drydock'
|
|
6
|
+
require 'onetime/cli'
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
class Definition
|
|
12
|
-
extend Drydock
|
|
13
|
-
|
|
14
|
-
default :share
|
|
15
|
-
|
|
16
|
-
global :H, String, "Base URI (e.g. https://onetimesecret.com/api)"
|
|
17
|
-
global :c, :custid, String, "Customer ID (e.g. you@yourcompany.com)"
|
|
18
|
-
global :k, :apikey, String, "API key (e.g. 4eb33c6340006d6607c813fc7e707a32f8bf5342)"
|
|
19
|
-
|
|
20
|
-
global :r, :recipient, Array, "Email address to deliver the secret link"
|
|
21
|
-
|
|
22
|
-
global :f, :format, String, "Output format (json or yaml)"
|
|
23
|
-
global :j, :json, "Shorthand for -f json"
|
|
24
|
-
global :y, :yaml, "Shorthand for -f yaml"
|
|
25
|
-
global :s, :string, "Shorthand for -f string (default)"
|
|
26
|
-
|
|
27
|
-
global :D, :debug do
|
|
28
|
-
OT::API.debug_output STDERR
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
global :V, :version do
|
|
32
|
-
puts OT::VERSION
|
|
33
|
-
exit 0
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
before do |obj|
|
|
37
|
-
OT::API.base_uri obj.global.H if obj.global.H
|
|
38
|
-
@api = OT::API.new obj.global.custid, obj.global.apikey
|
|
39
|
-
obj.global.format = 'yaml' if obj.global.yaml
|
|
40
|
-
obj.global.format = 'json' if obj.global.json
|
|
41
|
-
obj.global.format = 'string' if obj.global.string
|
|
42
|
-
obj.global.format = nil if obj.global.format == 'string'
|
|
43
|
-
if obj.global.format && !['json', 'yaml', 'csv'].member?(obj.global.format)
|
|
44
|
-
raise RuntimeError, "Unsupported format: #{obj.global.format}"
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
usage "onetime status"
|
|
49
|
-
command :status do |obj|
|
|
50
|
-
@res = @api.get '/status'
|
|
51
|
-
if @res.nil?
|
|
52
|
-
raise RuntimeError, 'Could not complete request'
|
|
53
|
-
elsif @api.response.code != 200
|
|
54
|
-
raise RuntimeError, OT::API.response_error_message(@res)
|
|
55
|
-
end
|
|
56
|
-
case obj.global.format
|
|
57
|
-
when 'json'
|
|
58
|
-
puts @res.to_json
|
|
59
|
-
when 'yaml'
|
|
60
|
-
puts @res.to_yaml
|
|
61
|
-
else
|
|
62
|
-
msg = @api.anonymous ? 'Anonymous' : @api.custid
|
|
63
|
-
STDERR.puts '# Host: %s' % OT::API.base_uri
|
|
64
|
-
STDERR.puts '# Account: %s' % msg
|
|
65
|
-
puts 'Service Status: %s' % @res[:status]
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
argv :key
|
|
70
|
-
usage "onetime receipt <KEY>"
|
|
71
|
-
command :receipt do |obj|
|
|
72
|
-
raise RuntimeError, "csv not supported" if obj.global.format == 'csv'
|
|
73
|
-
raise RuntimeError, "Usage: #{$0} receipt <KEY>" unless obj.argv.key
|
|
74
|
-
|
|
75
|
-
# Handle case where argv.key might be an array due to drydock parsing
|
|
76
|
-
receipt_key = Array(obj.argv.key).first.to_s
|
|
77
|
-
|
|
78
|
-
@res = @api.get '/receipt/%s' % receipt_key
|
|
79
|
-
if @res.nil?
|
|
80
|
-
raise RuntimeError, 'Could not complete request'
|
|
81
|
-
elsif @api.response.code != 200
|
|
82
|
-
raise RuntimeError, OT::API.response_error_message(@res)
|
|
83
|
-
end
|
|
84
|
-
case obj.global.format
|
|
85
|
-
when 'json'
|
|
86
|
-
puts @res.to_json
|
|
87
|
-
when 'yaml'
|
|
88
|
-
puts @res.to_yaml
|
|
89
|
-
else
|
|
90
|
-
puts @res.to_yaml
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
option :p, :passphrase, String, "Passphrase to decrypt the secret (only required if one was provided to you)"
|
|
95
|
-
argv :key
|
|
96
|
-
usage "onetime secret <KEY>"
|
|
97
|
-
usage "onetime secret [-p PASSPHRASE] <KEY>"
|
|
98
|
-
command :secret do |obj|
|
|
99
|
-
raise RuntimeError, "csv not supported" if obj.global.format == 'csv'
|
|
100
|
-
raise RuntimeError, "Usage: #{$0} secret <KEY>" unless obj.argv.key
|
|
101
|
-
|
|
102
|
-
# Handle case where argv.key might be an array due to drydock parsing
|
|
103
|
-
secret_key = Array(obj.argv.key).first.to_s
|
|
104
|
-
|
|
105
|
-
opts = { continue: true }
|
|
106
|
-
opts[:passphrase] = obj.option.passphrase if obj.option.passphrase
|
|
107
|
-
secret_key = OT::API.extract_secret_key(secret_key)
|
|
108
|
-
@res = @api.post '/secret/%s/reveal' % [secret_key], opts
|
|
109
|
-
if @res.nil?
|
|
110
|
-
raise RuntimeError, 'Could not complete request'
|
|
111
|
-
elsif @api.response.code != 200
|
|
112
|
-
raise RuntimeError, OT::API.response_error_message(@res)
|
|
113
|
-
end
|
|
114
|
-
case obj.global.format
|
|
115
|
-
when 'json'
|
|
116
|
-
puts @res.to_json
|
|
117
|
-
when 'yaml'
|
|
118
|
-
puts @res.to_yaml
|
|
119
|
-
else
|
|
120
|
-
value = @res.dig('record', 'secret_value')
|
|
121
|
-
puts value if value
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
command_alias :secret, :get
|
|
125
|
-
|
|
126
|
-
usage "onetime share"
|
|
127
|
-
usage "onetime share [-t 3600] [-p PASSPHRASE]"
|
|
128
|
-
usage "echo 'your secret' | onetime share"
|
|
129
|
-
usage "<path/2/file onetime share"
|
|
130
|
-
option :t, :ttl, Integer, "Time-to-live in seconds"
|
|
131
|
-
option :p, :passphrase, String, "Passphrase to encrypt the secret (something only you and recipient know)"
|
|
132
|
-
option :r, :recipient, Array, "Email address to deliver the secret link"
|
|
133
|
-
command :share do |obj|
|
|
134
|
-
recipients = [obj.option.recipient, obj.global.recipient].flatten.compact.uniq
|
|
135
|
-
begin
|
|
136
|
-
if Kernel.select [$stdin], nil, nil, 0
|
|
137
|
-
secret_value = STDIN.read
|
|
138
|
-
else
|
|
139
|
-
STDERR.puts "Paste message here (hit control-D to continue):"
|
|
140
|
-
secret_value = ARGF.read
|
|
141
|
-
STDERR.puts # new line to let the person know we got it.
|
|
142
|
-
end
|
|
143
|
-
rescue Interrupt => ex
|
|
144
|
-
puts "Exiting..."
|
|
145
|
-
exit 0
|
|
146
|
-
end
|
|
147
|
-
raise RuntimeError, "No secret provided" if secret_value.chomp.empty?
|
|
148
|
-
opts = { :secret => secret_value, :ttl => obj.option.ttl, :recipient => recipients }
|
|
149
|
-
opts[:passphrase] = obj.option.passphrase if obj.option.passphrase
|
|
150
|
-
@res = @api.post '/secret/conceal', opts
|
|
151
|
-
if @res.nil?
|
|
152
|
-
raise RuntimeError, 'Could not complete request'
|
|
153
|
-
elsif @api.response.code != 200
|
|
154
|
-
raise RuntimeError, OT::API.response_error_message(@res)
|
|
155
|
-
end
|
|
156
|
-
secret_key = OT::API.secret_key_from_response(@res)
|
|
157
|
-
raise RuntimeError, 'Unexpected response: missing record.secret.key' unless secret_key
|
|
158
|
-
uri = OT::API.web_uri('secret', secret_key)
|
|
159
|
-
case obj.global.format
|
|
160
|
-
when 'json'
|
|
161
|
-
puts @res.to_json
|
|
162
|
-
when 'yaml'
|
|
163
|
-
puts @res.to_yaml
|
|
164
|
-
else
|
|
165
|
-
recipients = OT::API.recipients_from_response(@res)
|
|
166
|
-
if !recipients.empty?
|
|
167
|
-
STDERR.puts '# Secret link sent to: %s' % recipients.join(',')
|
|
168
|
-
else
|
|
169
|
-
puts uri
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
usage "onetime generate"
|
|
175
|
-
usage "onetime generate [-t 3600] [-p PASSPHRASE]"
|
|
176
|
-
option :t, :ttl, Integer, "Time-to-live in seconds"
|
|
177
|
-
option :p, :passphrase, String, "Passphrase to encrypt the secret (something only you and recipient know)"
|
|
178
|
-
option :r, :recipient, Array, "Email address to deliver the secret link"
|
|
179
|
-
command :generate do |obj|
|
|
180
|
-
recipients = [obj.option.recipient, obj.global.recipient].flatten.compact.uniq
|
|
181
|
-
opts = { :ttl => obj.option.ttl, :recipient => recipients }
|
|
182
|
-
opts[:passphrase] = obj.option.passphrase if obj.option.passphrase
|
|
183
|
-
@res = @api.post '/secret/generate', opts
|
|
184
|
-
if @res.nil?
|
|
185
|
-
raise RuntimeError, 'Could not complete request'
|
|
186
|
-
elsif @api.response.code != 200
|
|
187
|
-
raise RuntimeError, OT::API.response_error_message(@res)
|
|
188
|
-
end
|
|
189
|
-
secret_key = OT::API.secret_key_from_response(@res)
|
|
190
|
-
raise RuntimeError, 'Unexpected response: missing record.secret.key' unless secret_key
|
|
191
|
-
uri = OT::API.web_uri('secret', secret_key)
|
|
192
|
-
case obj.global.format
|
|
193
|
-
when 'json'
|
|
194
|
-
puts @res.to_json
|
|
195
|
-
when 'yaml'
|
|
196
|
-
puts @res.to_yaml
|
|
197
|
-
when 'csv'
|
|
198
|
-
puts uri
|
|
199
|
-
else
|
|
200
|
-
recipients = OT::API.recipients_from_response(@res)
|
|
201
|
-
if !recipients.empty?
|
|
202
|
-
STDERR.puts '# Secret link sent to: %s' % recipients.join(',')
|
|
203
|
-
else
|
|
204
|
-
puts uri
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
begin
|
|
213
|
-
Drydock.run! ARGV, STDIN
|
|
214
|
-
rescue RuntimeError => ex
|
|
215
|
-
STDERR.puts ex.message
|
|
216
|
-
exit 1
|
|
217
|
-
rescue => ex
|
|
218
|
-
puts ex.message
|
|
219
|
-
puts ex.backtrace
|
|
220
|
-
exit 1
|
|
221
|
-
end
|
|
8
|
+
exit Onetime::CLI.run(ARGV)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
require 'optparse'
|
|
2
|
+
|
|
3
|
+
require_relative '../version'
|
|
4
|
+
|
|
5
|
+
module Onetime
|
|
6
|
+
module CLI
|
|
7
|
+
class Parser
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
|
|
10
|
+
DEFAULT_COMMAND = 'share'.freeze
|
|
11
|
+
KNOWN_COMMANDS = %w[status receipt secret share generate].freeze
|
|
12
|
+
COMMAND_ALIASES = { 'get' => 'secret' }.freeze
|
|
13
|
+
VALID_FORMATS = %w[json yaml csv].freeze
|
|
14
|
+
|
|
15
|
+
COMMAND_OPTIONS = {
|
|
16
|
+
'share' => %i[ttl passphrase recipient].freeze,
|
|
17
|
+
'generate' => %i[ttl passphrase recipient].freeze,
|
|
18
|
+
'secret' => %i[passphrase].freeze,
|
|
19
|
+
'receipt' => [].freeze,
|
|
20
|
+
'status' => [].freeze,
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
Result = Struct.new(
|
|
24
|
+
:command, :argv,
|
|
25
|
+
:base_uri, :custid, :apikey, :recipients,
|
|
26
|
+
:format, :debug, :show_version,
|
|
27
|
+
:ttl, :passphrase,
|
|
28
|
+
keyword_init: true,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def self.parse(argv)
|
|
32
|
+
new(argv).parse
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(argv)
|
|
36
|
+
@argv = argv.dup
|
|
37
|
+
@base_uri = nil
|
|
38
|
+
@custid = nil
|
|
39
|
+
@apikey = nil
|
|
40
|
+
@global_recipients = []
|
|
41
|
+
@command_recipients = []
|
|
42
|
+
@format = nil
|
|
43
|
+
@debug = false
|
|
44
|
+
@show_version = false
|
|
45
|
+
@ttl = nil
|
|
46
|
+
@passphrase = nil
|
|
47
|
+
@yaml_flag = false
|
|
48
|
+
@json_flag = false
|
|
49
|
+
@string_flag = false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse
|
|
53
|
+
global_option_parser.order!(@argv)
|
|
54
|
+
command, rest = extract_command
|
|
55
|
+
if command
|
|
56
|
+
command_option_parser(command).order!(rest)
|
|
57
|
+
end
|
|
58
|
+
apply_format_precedence!
|
|
59
|
+
normalize_format!
|
|
60
|
+
Result.new(
|
|
61
|
+
command: command,
|
|
62
|
+
argv: rest,
|
|
63
|
+
base_uri: @base_uri,
|
|
64
|
+
custid: @custid,
|
|
65
|
+
apikey: @apikey,
|
|
66
|
+
recipients: (@command_recipients + @global_recipients).uniq,
|
|
67
|
+
format: @format,
|
|
68
|
+
debug: @debug,
|
|
69
|
+
show_version: @show_version,
|
|
70
|
+
ttl: @ttl,
|
|
71
|
+
passphrase: @passphrase,
|
|
72
|
+
)
|
|
73
|
+
rescue OptionParser::ParseError => e
|
|
74
|
+
raise Error, e.message
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def global_option_parser
|
|
80
|
+
OptionParser.new do |opts|
|
|
81
|
+
opts.on('-H BASE_URI', String) { |v| @base_uri = v }
|
|
82
|
+
opts.on('-c CUSTID', '--custid CUSTID', String) { |v| @custid = v }
|
|
83
|
+
opts.on('-k APIKEY', '--apikey APIKEY', String) { |v| @apikey = v }
|
|
84
|
+
opts.on('-r RECIPIENT', '--recipient RECIPIENT', Array) { |v| @global_recipients.concat(Array(v)) }
|
|
85
|
+
opts.on('-f FORMAT', '--format FORMAT', String) { |v| @format = v }
|
|
86
|
+
opts.on('-j', '--json') { @json_flag = true }
|
|
87
|
+
opts.on('-y', '--yaml') { @yaml_flag = true }
|
|
88
|
+
opts.on('-s', '--string') { @string_flag = true }
|
|
89
|
+
opts.on('-D', '--debug') { @debug = true }
|
|
90
|
+
opts.on('-V', '--version') { @show_version = true }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def command_option_parser(command)
|
|
95
|
+
allowed = COMMAND_OPTIONS.fetch(command, [])
|
|
96
|
+
OptionParser.new do |opts|
|
|
97
|
+
if allowed.include?(:ttl)
|
|
98
|
+
opts.on('-t TTL', '--ttl TTL', Integer) { |v| @ttl = v }
|
|
99
|
+
end
|
|
100
|
+
if allowed.include?(:passphrase)
|
|
101
|
+
opts.on('-p PASSPHRASE', '--passphrase PASSPHRASE', String) { |v| @passphrase = v }
|
|
102
|
+
end
|
|
103
|
+
if allowed.include?(:recipient)
|
|
104
|
+
opts.on('-r RECIPIENT', '--recipient RECIPIENT', Array) { |v| @command_recipients.concat(Array(v)) }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def apply_format_precedence!
|
|
110
|
+
@format = 'yaml' if @yaml_flag
|
|
111
|
+
@format = 'json' if @json_flag
|
|
112
|
+
@format = 'string' if @string_flag
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def normalize_format!
|
|
116
|
+
@format = nil if @format == 'string'
|
|
117
|
+
return if @format.nil?
|
|
118
|
+
return if VALID_FORMATS.include?(@format)
|
|
119
|
+
raise Error, "Unsupported format: #{@format}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def extract_command
|
|
123
|
+
return [nil, []] if @show_version
|
|
124
|
+
|
|
125
|
+
raw = @argv.shift || DEFAULT_COMMAND
|
|
126
|
+
canonical = COMMAND_ALIASES.fetch(raw, raw)
|
|
127
|
+
unless KNOWN_COMMANDS.include?(canonical)
|
|
128
|
+
raise Error, "unknown command: #{raw}"
|
|
129
|
+
end
|
|
130
|
+
[canonical, @argv]
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'yaml'
|
|
3
|
+
|
|
4
|
+
require_relative '../api'
|
|
5
|
+
require_relative '../version'
|
|
6
|
+
|
|
7
|
+
module Onetime
|
|
8
|
+
module CLI
|
|
9
|
+
class Runner
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
|
|
12
|
+
PROGRAM_NAME = 'onetime'.freeze
|
|
13
|
+
|
|
14
|
+
def initialize(parsed, stdin: $stdin, stdout: $stdout, stderr: $stderr)
|
|
15
|
+
@parsed = parsed
|
|
16
|
+
@stdin = stdin
|
|
17
|
+
@stdout = stdout
|
|
18
|
+
@stderr = stderr
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run
|
|
22
|
+
return print_version if @parsed.show_version
|
|
23
|
+
|
|
24
|
+
configure_api!
|
|
25
|
+
case @parsed.command
|
|
26
|
+
when 'status' then run_status
|
|
27
|
+
when 'receipt' then run_receipt
|
|
28
|
+
when 'secret' then run_secret
|
|
29
|
+
when 'share' then run_share
|
|
30
|
+
when 'generate' then run_generate
|
|
31
|
+
else
|
|
32
|
+
raise Error, "unhandled command: #{@parsed.command.inspect}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def print_version
|
|
39
|
+
@stdout.puts Onetime::VERSION
|
|
40
|
+
0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def configure_api!
|
|
44
|
+
OT::API.base_uri @parsed.base_uri if @parsed.base_uri
|
|
45
|
+
OT::API.debug_output @stderr if @parsed.debug
|
|
46
|
+
@api = OT::API.new(@parsed.custid, @parsed.apikey)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def run_status
|
|
50
|
+
res = api_get('/status')
|
|
51
|
+
case @parsed.format
|
|
52
|
+
when 'json'
|
|
53
|
+
@stdout.puts res.to_json
|
|
54
|
+
when 'yaml'
|
|
55
|
+
@stdout.puts res.to_yaml
|
|
56
|
+
else
|
|
57
|
+
account = @api.anonymous ? 'Anonymous' : @api.custid
|
|
58
|
+
@stderr.puts '# Host: %s' % OT::API.base_uri
|
|
59
|
+
@stderr.puts '# Account: %s' % account
|
|
60
|
+
@stdout.puts 'Service Status: %s' % res[:status]
|
|
61
|
+
end
|
|
62
|
+
0
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def run_receipt
|
|
66
|
+
raise Error, 'csv not supported' if @parsed.format == 'csv'
|
|
67
|
+
raise Error, "Usage: #{PROGRAM_NAME} receipt <KEY>" if @parsed.argv.empty?
|
|
68
|
+
|
|
69
|
+
key = @parsed.argv.first.to_s
|
|
70
|
+
res = api_get('/receipt/%s' % key)
|
|
71
|
+
case @parsed.format
|
|
72
|
+
when 'json'
|
|
73
|
+
@stdout.puts res.to_json
|
|
74
|
+
else
|
|
75
|
+
@stdout.puts res.to_yaml
|
|
76
|
+
end
|
|
77
|
+
0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def run_secret
|
|
81
|
+
raise Error, 'csv not supported' if @parsed.format == 'csv'
|
|
82
|
+
raise Error, "Usage: #{PROGRAM_NAME} secret <KEY>" if @parsed.argv.empty?
|
|
83
|
+
|
|
84
|
+
raw_key = @parsed.argv.first.to_s
|
|
85
|
+
key = OT::API.extract_secret_key(raw_key)
|
|
86
|
+
opts = { continue: true }
|
|
87
|
+
opts[:passphrase] = @parsed.passphrase if @parsed.passphrase
|
|
88
|
+
res = api_post('/secret/%s/reveal' % key, opts)
|
|
89
|
+
case @parsed.format
|
|
90
|
+
when 'json'
|
|
91
|
+
@stdout.puts res.to_json
|
|
92
|
+
when 'yaml'
|
|
93
|
+
@stdout.puts res.to_yaml
|
|
94
|
+
else
|
|
95
|
+
value = res.dig('record', 'secret_value')
|
|
96
|
+
@stdout.puts value if value
|
|
97
|
+
end
|
|
98
|
+
0
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def run_share
|
|
102
|
+
begin
|
|
103
|
+
secret_value = read_share_input
|
|
104
|
+
rescue Interrupt
|
|
105
|
+
@stdout.puts 'Exiting...'
|
|
106
|
+
return 0
|
|
107
|
+
end
|
|
108
|
+
raise Error, 'No secret provided' if secret_value.chomp.empty?
|
|
109
|
+
|
|
110
|
+
opts = { secret: secret_value, ttl: @parsed.ttl, recipient: @parsed.recipients }
|
|
111
|
+
opts[:passphrase] = @parsed.passphrase if @parsed.passphrase
|
|
112
|
+
res = api_post('/secret/conceal', opts)
|
|
113
|
+
emit_share_result(res)
|
|
114
|
+
0
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def run_generate
|
|
118
|
+
unless @parsed.argv.empty?
|
|
119
|
+
extras = @parsed.argv
|
|
120
|
+
raise Error, "generate takes no arguments (got: %s). Did you mean: onetime share < %s" % [extras.join(' '), extras.first]
|
|
121
|
+
end
|
|
122
|
+
if !@stdin.tty? && !@stdin.eof?
|
|
123
|
+
raise Error, 'generate does not read stdin. Did you mean: onetime share'
|
|
124
|
+
end
|
|
125
|
+
opts = { ttl: @parsed.ttl, recipient: @parsed.recipients }
|
|
126
|
+
opts[:passphrase] = @parsed.passphrase if @parsed.passphrase
|
|
127
|
+
res = api_post('/secret/generate', opts)
|
|
128
|
+
emit_generate_result(res)
|
|
129
|
+
0
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def read_share_input
|
|
133
|
+
if !@parsed.argv.empty?
|
|
134
|
+
read_argv_inputs(@parsed.argv)
|
|
135
|
+
elsif !@stdin.tty?
|
|
136
|
+
@stdin.read
|
|
137
|
+
else
|
|
138
|
+
@stderr.puts 'Paste message here (hit control-D to continue):'
|
|
139
|
+
content = @stdin.read
|
|
140
|
+
@stderr.puts
|
|
141
|
+
content
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def read_argv_inputs(paths)
|
|
146
|
+
paths.map { |path| path == '-' ? @stdin.read : File.read(path) }.join
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def emit_share_result(res)
|
|
150
|
+
secret_key = OT::API.secret_key_from_response(res)
|
|
151
|
+
raise Error, 'Unexpected response: missing record.secret.key' unless secret_key
|
|
152
|
+
|
|
153
|
+
uri = OT::API.web_uri('secret', secret_key)
|
|
154
|
+
case @parsed.format
|
|
155
|
+
when 'json'
|
|
156
|
+
@stdout.puts res.to_json
|
|
157
|
+
when 'yaml'
|
|
158
|
+
@stdout.puts res.to_yaml
|
|
159
|
+
else
|
|
160
|
+
recipients = OT::API.recipients_from_response(res)
|
|
161
|
+
if !recipients.empty?
|
|
162
|
+
@stderr.puts '# Secret link sent to: %s' % recipients.join(',')
|
|
163
|
+
else
|
|
164
|
+
@stdout.puts uri
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def emit_generate_result(res)
|
|
170
|
+
secret_key = OT::API.secret_key_from_response(res)
|
|
171
|
+
raise Error, 'Unexpected response: missing record.secret.key' unless secret_key
|
|
172
|
+
|
|
173
|
+
uri = OT::API.web_uri('secret', secret_key)
|
|
174
|
+
case @parsed.format
|
|
175
|
+
when 'json'
|
|
176
|
+
@stdout.puts res.to_json
|
|
177
|
+
when 'yaml'
|
|
178
|
+
@stdout.puts res.to_yaml
|
|
179
|
+
when 'csv'
|
|
180
|
+
@stdout.puts uri
|
|
181
|
+
else
|
|
182
|
+
recipients = OT::API.recipients_from_response(res)
|
|
183
|
+
if !recipients.empty?
|
|
184
|
+
@stderr.puts '# Secret link sent to: %s' % recipients.join(',')
|
|
185
|
+
else
|
|
186
|
+
@stdout.puts uri
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def api_get(path)
|
|
192
|
+
res = @api.get(path)
|
|
193
|
+
check_response!(res)
|
|
194
|
+
res
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def api_post(path, params)
|
|
198
|
+
res = @api.post(path, params)
|
|
199
|
+
check_response!(res)
|
|
200
|
+
res
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def check_response!(res)
|
|
204
|
+
raise Error, 'Could not complete request' if res.nil?
|
|
205
|
+
return if @api.response.code == 200
|
|
206
|
+
raise Error, OT::API.response_error_message(res)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
data/lib/onetime/cli.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require_relative 'api'
|
|
2
|
+
require_relative 'version'
|
|
3
|
+
require_relative 'cli/parser'
|
|
4
|
+
require_relative 'cli/runner'
|
|
5
|
+
|
|
6
|
+
module Onetime
|
|
7
|
+
module CLI
|
|
8
|
+
def self.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr)
|
|
9
|
+
parsed = Parser.parse(argv)
|
|
10
|
+
Runner.new(parsed, stdin: stdin, stdout: stdout, stderr: stderr).run
|
|
11
|
+
rescue Parser::Error, Runner::Error, RuntimeError => e
|
|
12
|
+
stderr.puts e.message
|
|
13
|
+
1
|
|
14
|
+
rescue StandardError => e
|
|
15
|
+
stdout.puts e.message
|
|
16
|
+
stdout.puts e.backtrace
|
|
17
|
+
1
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/onetime/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: onetime-up
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Delano Mandelbaum
|
|
@@ -10,20 +10,6 @@ bindir: bin
|
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 2026-05-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
-
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: drydock
|
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
|
16
|
-
requirements:
|
|
17
|
-
- - "~>"
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: 1.0.0
|
|
20
|
-
type: :runtime
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - "~>"
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: 1.0.0
|
|
27
13
|
- !ruby/object:Gem::Dependency
|
|
28
14
|
name: httparty
|
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -82,6 +68,9 @@ files:
|
|
|
82
68
|
- README.md
|
|
83
69
|
- bin/onetime
|
|
84
70
|
- lib/onetime/api.rb
|
|
71
|
+
- lib/onetime/cli.rb
|
|
72
|
+
- lib/onetime/cli/parser.rb
|
|
73
|
+
- lib/onetime/cli/runner.rb
|
|
85
74
|
- lib/onetime/version.rb
|
|
86
75
|
homepage: https://github.com/onetimesecret/onetime-ruby-up
|
|
87
76
|
licenses:
|
|
@@ -102,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
102
91
|
- !ruby/object:Gem::Version
|
|
103
92
|
version: 2.0.0
|
|
104
93
|
requirements: []
|
|
105
|
-
rubygems_version: 4.0.
|
|
94
|
+
rubygems_version: 4.0.9
|
|
106
95
|
specification_version: 4
|
|
107
96
|
summary: Command-line tool and library for onetimesecret.com API (API v2)
|
|
108
97
|
test_files: []
|