onetime-up 0.7.0 → 0.8.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 608460e67b5939792b49aad0117baa385911125337337624113f4e1a95388063
4
- data.tar.gz: ff189cbd5cd36d19e102dea6dcd16acc2015a7de13400b5c42bd2638152e4533
3
+ metadata.gz: 44363e304d46201be0d1362d29b83afc834932fca947814d8ff641e852906122
4
+ data.tar.gz: cca803e708264e825b106274a6e07732991c84e2c6c1036d77997ca26a5671cc
5
5
  SHA512:
6
- metadata.gz: dfd25117746474d2ccb452aaeeaa5dcbb4b8ebe3c4a28c078ed11f33c2011bb19d60a25058be2a1fa5e9f98a9e56da75a4a5c877a7c559b6305f40753f38c031
7
- data.tar.gz: 66b1255b1dd1d9f8c18e7e617daea8034ad518a0391f230276cb93a919a054a733fd2d2040ce3086c012ac29fc0dc76a824578e5e2f27acc2f584f2cbe3bfbf2
6
+ metadata.gz: 0f4e96784f5e441a3d9625b56a0f8a5a318a27bd12109535f2e50ee5dd5e89ee9e7b35f21334062952f19ad3059d7b2ca08c95144d2f6206f5f203e21fb7f62e
7
+ data.tar.gz: f4eccb25d18d9c504b9f8c06a12b7a11520b9a8e0022d296caacde3e7ca1651e5017972c853feb74e7814b2608b4103415fa709749e639711ddbc2a5ed626a41
data/CHANGELOG.md CHANGED
@@ -8,6 +8,10 @@
8
8
  - Restore `apiversion` path handling for callers that pass an API version explicitly
9
9
  - Add safer CLI response handling for changed or partial API responses
10
10
  - Accept secret URLs from root and regional onetimesecret.com hosts
11
+ - `generate` no longer hangs when stdin is an idle open pipe (e.g. ssh, cron)
12
+ - Accept secret URLs with a trailing slash
13
+ - `-H` takes precedence over `ONETIME_HOST`
14
+ - Report non-JSON API responses with their HTTP status instead of a backtrace
11
15
 
12
16
  ## [0.6.0] - 2026-01-18
13
17
 
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,226 +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 'uri'
7
- require 'onetime/api'
8
- require 'drydock'
6
+ require 'onetime/cli'
9
7
 
10
- class Onetime::CLI
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
- extra_args = Array(obj.argv).flatten.compact
181
- unless extra_args.empty?
182
- raise RuntimeError, "generate takes no arguments (got: %s). Did you mean: onetime share < %s" % [extra_args.join(' '), extra_args.first]
183
- end
184
- if !$stdin.tty? && !$stdin.eof?
185
- raise RuntimeError, "generate does not read stdin. Did you mean: onetime share"
186
- end
187
- recipients = [obj.option.recipient, obj.global.recipient].flatten.compact.uniq
188
- opts = { :ttl => obj.option.ttl, :recipient => recipients }
189
- opts[:passphrase] = obj.option.passphrase if obj.option.passphrase
190
- @res = @api.post '/secret/generate', opts
191
- if @res.nil?
192
- raise RuntimeError, 'Could not complete request'
193
- elsif @api.response.code != 200
194
- raise RuntimeError, OT::API.response_error_message(@res)
195
- end
196
- secret_key = OT::API.secret_key_from_response(@res)
197
- raise RuntimeError, 'Unexpected response: missing record.secret.key' unless secret_key
198
- uri = OT::API.web_uri('secret', secret_key)
199
- case obj.global.format
200
- when 'json'
201
- puts @res.to_json
202
- when 'yaml'
203
- puts @res.to_yaml
204
- when 'csv'
205
- puts uri
206
- else
207
- recipients = OT::API.recipients_from_response(@res)
208
- if !recipients.empty?
209
- STDERR.puts '# Secret link sent to: %s' % recipients.join(',')
210
- else
211
- puts uri
212
- end
213
- end
214
- end
215
-
216
- end
217
- end
218
-
219
- begin
220
- Drydock.run! ARGV, STDIN
221
- rescue RuntimeError => ex
222
- STDERR.puts ex.message
223
- exit 1
224
- rescue => ex
225
- puts ex.message
226
- puts ex.backtrace
227
- exit 1
228
- end
8
+ exit Onetime::CLI.run(ARGV)
data/lib/onetime/api.rb CHANGED
@@ -146,7 +146,7 @@ module Onetime
146
146
  return value unless uri.host && uri.path
147
147
  return value unless accepted_secret_host?(uri.host, api_base_uri)
148
148
 
149
- match = uri.path.match(%r{\A/secret/([a-zA-Z0-9]+)\z})
149
+ match = uri.path.match(%r{\A/secret/([a-zA-Z0-9]+)/?\z})
150
150
  match ? match[1] : value
151
151
  rescue URI::InvalidURIError
152
152
  value
@@ -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,216 @@
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.debug_output @stderr if @parsed.debug
45
+ @api = OT::API.new(@parsed.custid, @parsed.apikey)
46
+ # Set after API.new, which applies ONETIME_HOST, so that -H takes precedence.
47
+ OT::API.base_uri @parsed.base_uri if @parsed.base_uri
48
+ end
49
+
50
+ def run_status
51
+ res = api_get('/status')
52
+ case @parsed.format
53
+ when 'json'
54
+ @stdout.puts res.to_json
55
+ when 'yaml'
56
+ @stdout.puts res.to_yaml
57
+ else
58
+ account = @api.anonymous ? 'Anonymous' : @api.custid
59
+ @stderr.puts '# Host: %s' % OT::API.base_uri
60
+ @stderr.puts '# Account: %s' % account
61
+ @stdout.puts 'Service Status: %s' % res[:status]
62
+ end
63
+ 0
64
+ end
65
+
66
+ def run_receipt
67
+ raise Error, 'csv not supported' if @parsed.format == 'csv'
68
+ raise Error, "Usage: #{PROGRAM_NAME} receipt <KEY>" if @parsed.argv.empty?
69
+
70
+ key = @parsed.argv.first.to_s
71
+ res = api_get('/receipt/%s' % key)
72
+ case @parsed.format
73
+ when 'json'
74
+ @stdout.puts res.to_json
75
+ else
76
+ @stdout.puts res.to_yaml
77
+ end
78
+ 0
79
+ end
80
+
81
+ def run_secret
82
+ raise Error, 'csv not supported' if @parsed.format == 'csv'
83
+ raise Error, "Usage: #{PROGRAM_NAME} secret <KEY>" if @parsed.argv.empty?
84
+
85
+ raw_key = @parsed.argv.first.to_s
86
+ key = OT::API.extract_secret_key(raw_key)
87
+ opts = { continue: true }
88
+ opts[:passphrase] = @parsed.passphrase if @parsed.passphrase
89
+ res = api_post('/secret/%s/reveal' % key, opts)
90
+ case @parsed.format
91
+ when 'json'
92
+ @stdout.puts res.to_json
93
+ when 'yaml'
94
+ @stdout.puts res.to_yaml
95
+ else
96
+ value = res.dig('record', 'secret_value')
97
+ @stdout.puts value if value
98
+ end
99
+ 0
100
+ end
101
+
102
+ def run_share
103
+ begin
104
+ secret_value = read_share_input
105
+ rescue Interrupt
106
+ @stdout.puts 'Exiting...'
107
+ return 0
108
+ end
109
+ raise Error, 'No secret provided' if secret_value.chomp.empty?
110
+
111
+ opts = { secret: secret_value, ttl: @parsed.ttl, recipient: @parsed.recipients }
112
+ opts[:passphrase] = @parsed.passphrase if @parsed.passphrase
113
+ res = api_post('/secret/conceal', opts)
114
+ emit_share_result(res)
115
+ 0
116
+ end
117
+
118
+ def run_generate
119
+ unless @parsed.argv.empty?
120
+ extras = @parsed.argv
121
+ raise Error, "generate takes no arguments (got: %s). Did you mean: onetime share < %s" % [extras.join(' '), extras.first]
122
+ end
123
+ # Non-blocking check: an idle pipe that is never closed (ssh, cron) must not hang.
124
+ if !@stdin.tty? && IO.select([@stdin], nil, nil, 0) && !@stdin.eof?
125
+ raise Error, 'generate does not read stdin. Did you mean: onetime share'
126
+ end
127
+ opts = { ttl: @parsed.ttl, recipient: @parsed.recipients }
128
+ opts[:passphrase] = @parsed.passphrase if @parsed.passphrase
129
+ res = api_post('/secret/generate', opts)
130
+ emit_generate_result(res)
131
+ 0
132
+ end
133
+
134
+ def read_share_input
135
+ if !@parsed.argv.empty?
136
+ read_argv_inputs(@parsed.argv)
137
+ elsif !@stdin.tty?
138
+ @stdin.read
139
+ else
140
+ @stderr.puts 'Paste message here (hit control-D to continue):'
141
+ content = @stdin.read
142
+ @stderr.puts
143
+ content
144
+ end
145
+ end
146
+
147
+ def read_argv_inputs(paths)
148
+ paths.map { |path| path == '-' ? @stdin.read : File.read(path) }.join
149
+ end
150
+
151
+ def emit_share_result(res)
152
+ secret_key = OT::API.secret_key_from_response(res)
153
+ raise Error, 'Unexpected response: missing record.secret.key' unless secret_key
154
+
155
+ uri = OT::API.web_uri('secret', secret_key)
156
+ case @parsed.format
157
+ when 'json'
158
+ @stdout.puts res.to_json
159
+ when 'yaml'
160
+ @stdout.puts res.to_yaml
161
+ else
162
+ recipients = OT::API.recipients_from_response(res)
163
+ if !recipients.empty?
164
+ @stderr.puts '# Secret link sent to: %s' % recipients.join(',')
165
+ else
166
+ @stdout.puts uri
167
+ end
168
+ end
169
+ end
170
+
171
+ def emit_generate_result(res)
172
+ secret_key = OT::API.secret_key_from_response(res)
173
+ raise Error, 'Unexpected response: missing record.secret.key' unless secret_key
174
+
175
+ uri = OT::API.web_uri('secret', secret_key)
176
+ case @parsed.format
177
+ when 'json'
178
+ @stdout.puts res.to_json
179
+ when 'yaml'
180
+ @stdout.puts res.to_yaml
181
+ when 'csv'
182
+ @stdout.puts uri
183
+ else
184
+ recipients = OT::API.recipients_from_response(res)
185
+ if !recipients.empty?
186
+ @stderr.puts '# Secret link sent to: %s' % recipients.join(',')
187
+ else
188
+ @stdout.puts uri
189
+ end
190
+ end
191
+ end
192
+
193
+ def api_get(path)
194
+ request { @api.get(path) }
195
+ end
196
+
197
+ def api_post(path, params)
198
+ request { @api.post(path, params) }
199
+ end
200
+
201
+ def request
202
+ res = yield
203
+ check_response!(res)
204
+ res
205
+ rescue JSON::ParserError
206
+ raise Error, "Unexpected non-JSON response (HTTP #{@api.response.code})"
207
+ end
208
+
209
+ def check_response!(res)
210
+ raise Error, 'Could not complete request' if res.nil?
211
+ return if @api.response.code == 200
212
+ raise Error, OT::API.response_error_message(res)
213
+ end
214
+ end
215
+ end
216
+ end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Onetime
2
- VERSION = "0.7.0"
2
+ VERSION = "0.8.1"
3
3
  end
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: onetime-up
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
8
8
  - Saverio Miroddi
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-13 00:00:00.000000000 Z
11
+ date: 2026-10-06 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.9
94
+ rubygems_version: 4.0.20
106
95
  specification_version: 4
107
96
  summary: Command-line tool and library for onetimesecret.com API (API v2)
108
97
  test_files: []