onetime-up 0.6.0 → 0.6.2

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: daf373c819502e6a34a5a85fe033e39323cec0d5136af81b394fcd597171601f
4
- data.tar.gz: 9c5408c3eed4b30065a4093a0e2302fd41f96a127607feaef0e72200b534c3de
3
+ metadata.gz: 829b100c0882cdb8e5f797d27d0f9e52e576cd20f20235f7836532f3872b74ea
4
+ data.tar.gz: 9df7bd8e315d3fe6339619016b1b03c0450bbfb9c6a83644f1ce9bcf77e77fd6
5
5
  SHA512:
6
- metadata.gz: a3149a2f9bfe3112ebcdcbaedabb184d42e0849781b589a8bfaf54a001b5f1b97437d922b59b31911c087563b2182f69761c4e1b34325637c9a0a4090132733d
7
- data.tar.gz: 5032db644622b154dfd770a974befb924a9bed99c2f0548e3f4f1abf417a9296355b89b09b241d28f3ee645dfb69e1042205fd4abb6bf005bc3ac4819c87d12c
6
+ metadata.gz: 905c34dd750efb26e2d3af6314d22b30d8a101885a9eef965d730a2c71849b80b54e6ddf6bad8f952c1de8a4a822b1df0f26667dc7b4cc883fec0efe25221d6d
7
+ data.tar.gz: 53e8f305c78a4a79a539adb359508078835ae83f96156f8c9abd503ff7adce81d758411d86efffb30833eae46588ff5e8d6937dd316892020bc5291712d60f44
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ### Fixed
6
+
7
+ - Update library callers to API v2 response shapes (`record.receipt`, `record.secret`, reveal `record.secret_value`)
8
+ - Restore `apiversion` path handling for callers that pass an API version explicitly
9
+ - Add safer CLI response handling for changed or partial API responses
10
+ - Accept secret URLs from root and regional onetimesecret.com hosts
11
+
3
12
  ## [0.6.0] - 2026-01-18
4
13
 
5
14
  ### Added
@@ -8,9 +17,11 @@ Major/breaking changes:
8
17
 
9
18
  - Upgrade to API v2
10
19
  - Rename command "metadata" to "receipt"
20
+ - Change the default API host from the root service to the EU region (`https://eu.onetimesecret.com/api`)
11
21
 
12
22
  Other changes:
13
23
 
24
+ - Add opt-in official API contract validation for v2 endpoint shapes
14
25
  - Remove Rakefile, signing data and Jeweler references
15
26
  - Simplified and modernized gemspec (set minimum Ruby to 3.2)
16
27
  - Add Gemfile
data/README.md CHANGED
@@ -2,8 +2,43 @@
2
2
 
3
3
  Fork of the Ruby program [`One-Time Secret`](https://github.com/onetimesecret/onetime-ruby), using the API v2.
4
4
 
5
- See the original website for usage (commands are unchanged).
5
+ ## Basic usage
6
+
7
+ Share a secret:
8
+
9
+ ```sh
10
+ echo "secret text" | onetime share
11
+ ```
12
+
13
+ Share a secret protected by a passphrase:
14
+
15
+ ```sh
16
+ echo "secret text" | onetime share -p "shared-passphrase"
17
+ ```
18
+
19
+ Retrieve a secret:
20
+
21
+ ```sh
22
+ onetime secret SECRET_KEY
23
+ ```
24
+
25
+ Generate a random secret:
26
+
27
+ ```sh
28
+ onetime generate
29
+ ```
30
+
31
+ Use JSON or YAML output:
32
+
33
+ ```sh
34
+ onetime -j generate
35
+ onetime -y receipt RECEIPT_KEY
36
+ ```
6
37
 
7
38
  ## Breaking changes
8
39
 
9
40
  The command `metadata` has been renamed `receipt` for consistency with the API.
41
+
42
+ The default API host is now `https://eu.onetimesecret.com/api`.
43
+
44
+ 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`.
data/bin/onetime CHANGED
@@ -51,7 +51,7 @@ class Onetime::CLI
51
51
  if @res.nil?
52
52
  raise RuntimeError, 'Could not complete request'
53
53
  elsif @api.response.code != 200
54
- raise RuntimeError, @res['message']
54
+ raise RuntimeError, OT::API.response_error_message(@res)
55
55
  end
56
56
  case obj.global.format
57
57
  when 'json'
@@ -71,11 +71,15 @@ class Onetime::CLI
71
71
  command :receipt do |obj|
72
72
  raise RuntimeError, "csv not supported" if obj.global.format == 'csv'
73
73
  raise RuntimeError, "Usage: #{$0} receipt <KEY>" unless obj.argv.key
74
- @res = @api.get '/receipt/%s' % 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
75
79
  if @res.nil?
76
80
  raise RuntimeError, 'Could not complete request'
77
81
  elsif @api.response.code != 200
78
- raise RuntimeError, @res['message']
82
+ raise RuntimeError, OT::API.response_error_message(@res)
79
83
  end
80
84
  case obj.global.format
81
85
  when 'json'
@@ -94,17 +98,18 @@ class Onetime::CLI
94
98
  command :secret do |obj|
95
99
  raise RuntimeError, "csv not supported" if obj.global.format == 'csv'
96
100
  raise RuntimeError, "Usage: #{$0} secret <KEY>" unless obj.argv.key
97
- opts = {}
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 }
98
106
  opts[:passphrase] = obj.option.passphrase if obj.option.passphrase
99
- base_uri = URI.parse Onetime::API.base_uri
100
- if obj.argv.key =~ /#{base_uri.hostname}\/secret\/([a-zA-Z0-9]+)/
101
- obj.argv.key = $1
102
- end
103
- @res = @api.post '/secret/%s/reveal' % [obj.argv.key], opts
107
+ secret_key = OT::API.extract_secret_key(secret_key)
108
+ @res = @api.post '/secret/%s/reveal' % [secret_key], opts
104
109
  if @res.nil?
105
110
  raise RuntimeError, 'Could not complete request'
106
111
  elsif @api.response.code != 200
107
- raise RuntimeError, @res['message']
112
+ raise RuntimeError, OT::API.response_error_message(@res)
108
113
  end
109
114
  case obj.global.format
110
115
  when 'json'
@@ -112,7 +117,7 @@ class Onetime::CLI
112
117
  when 'yaml'
113
118
  puts @res.to_yaml
114
119
  else
115
- value = @res['record'] && @res['record']['value']
120
+ value = @res.dig('record', 'secret_value')
116
121
  puts value if value
117
122
  end
118
123
  end
@@ -146,9 +151,10 @@ class Onetime::CLI
146
151
  if @res.nil?
147
152
  raise RuntimeError, 'Could not complete request'
148
153
  elsif @api.response.code != 200
149
- raise RuntimeError, @res['message']
154
+ raise RuntimeError, OT::API.response_error_message(@res)
150
155
  end
151
- secret_key = @res['record']['secret']['key']
156
+ secret_key = OT::API.secret_key_from_response(@res)
157
+ raise RuntimeError, 'Unexpected response: missing record.secret.key' unless secret_key
152
158
  uri = OT::API.web_uri('secret', secret_key)
153
159
  case obj.global.format
154
160
  when 'json'
@@ -156,9 +162,9 @@ class Onetime::CLI
156
162
  when 'yaml'
157
163
  puts @res.to_yaml
158
164
  else
159
- recipient = @res['details']['recipient']
160
- if recipient && !recipient.compact.empty?
161
- STDERR.puts '# Secret link sent to: %s' % recipient.join(',')
165
+ recipients = OT::API.recipients_from_response(@res)
166
+ if !recipients.empty?
167
+ STDERR.puts '# Secret link sent to: %s' % recipients.join(',')
162
168
  else
163
169
  puts uri
164
170
  end
@@ -178,9 +184,10 @@ class Onetime::CLI
178
184
  if @res.nil?
179
185
  raise RuntimeError, 'Could not complete request'
180
186
  elsif @api.response.code != 200
181
- raise RuntimeError, @res['message']
187
+ raise RuntimeError, OT::API.response_error_message(@res)
182
188
  end
183
- secret_key = @res['record']['secret']['key']
189
+ secret_key = OT::API.secret_key_from_response(@res)
190
+ raise RuntimeError, 'Unexpected response: missing record.secret.key' unless secret_key
184
191
  uri = OT::API.web_uri('secret', secret_key)
185
192
  case obj.global.format
186
193
  when 'json'
@@ -190,9 +197,9 @@ class Onetime::CLI
190
197
  when 'csv'
191
198
  puts uri
192
199
  else
193
- recipient = @res['details']['recipient']
194
- if recipient && !recipient.compact.empty?
195
- STDERR.puts '# Secret link sent to: %s' % recipient.join(',')
200
+ recipients = OT::API.recipients_from_response(@res)
201
+ if !recipients.empty?
202
+ STDERR.puts '# Secret link sent to: %s' % recipients.join(',')
196
203
  else
197
204
  puts uri
198
205
  end
data/lib/onetime/api.rb CHANGED
@@ -41,11 +41,12 @@ module Onetime
41
41
  base_uri 'https://eu.onetimesecret.com/api'
42
42
  format :json
43
43
  headers 'X-Onetime-Client' => 'ruby: %s/%s' % [RUBY_VERSION, Onetime::VERSION]
44
- attr_reader :opts, :response, :custid, :key, :default_params, :anonymous
44
+ attr_reader :opts, :response, :custid, :key, :default_params, :anonymous, :apiversion
45
45
  def initialize custid=nil, key=nil, opts={}
46
46
  unless ENV['ONETIME_HOST'].to_s.empty?
47
47
  self.class.base_uri ENV['ONETIME_HOST']
48
48
  end
49
+ @apiversion = opts.delete(:apiversion) || opts.delete('apiversion') || 2
49
50
  @opts = opts
50
51
  @default_params = {}
51
52
  @custid = custid || ENV['ONETIME_CUSTID']
@@ -65,8 +66,15 @@ module Onetime
65
66
  opts[:query] = (params || {}).merge default_params
66
67
  execute_request :get, path, opts
67
68
  end
68
- def post path, params=nil
69
+ def post path, params=nil, request_opts={}
69
70
  opts = self.opts.clone
71
+ wrap = if request_opts.key?(:wrap)
72
+ request_opts[:wrap]
73
+ elsif request_opts.key?('wrap')
74
+ request_opts['wrap']
75
+ else
76
+ :auto
77
+ end
70
78
  body_params = (params || {}).merge default_params
71
79
 
72
80
  # V2 API uses JSON format
@@ -75,9 +83,7 @@ module Onetime
75
83
  'Accept' => 'application/json'
76
84
  })
77
85
 
78
- # Only /secret/conceal and /secret/generate wrap params in "secret" key
79
- # Other endpoints (reveal, burn, etc.) do NOT wrap
80
- if path =~ /\/secret\/(conceal|generate)$/
86
+ if wrap_secret_body?(path, wrap)
81
87
  body_params = { secret: body_params }
82
88
  end
83
89
 
@@ -86,16 +92,21 @@ module Onetime
86
92
  execute_request :post, path, opts
87
93
  end
88
94
  def api_path *args
89
- args.unshift ['', 'v2'] # force leading slash and v2 version
95
+ args.unshift ['', "v#{apiversion}"] # force leading slash and version
90
96
  path = args.flatten.join('/')
91
97
  path.gsub(/\/+/, '/')
92
98
  end
93
99
  private
100
+ def wrap_secret_body?(path, wrap)
101
+ return false if wrap == false || wrap.nil?
102
+ return true if wrap == :secret
103
+ api_path(path).match?(%r{\A/v\d+/secret/(conceal|generate)/?\z})
104
+ end
105
+
94
106
  def execute_request meth, path, opts
95
107
  path = api_path [path]
96
108
  @response = self.class.send meth, path, opts
97
- result = self.class.indifferent_params @response.parsed_response
98
- result
109
+ self.class.indifferent_params @response.parsed_response
99
110
  end
100
111
  class << self
101
112
  def web_uri *args
@@ -128,6 +139,55 @@ module Onetime
128
139
  def indifferent_hash
129
140
  Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
130
141
  end
142
+ def extract_secret_key(value, api_base_uri=base_uri)
143
+ return value unless value
144
+
145
+ uri = URI.parse(value.to_s)
146
+ return value unless uri.host && uri.path
147
+ return value unless accepted_secret_host?(uri.host, api_base_uri)
148
+
149
+ match = uri.path.match(%r{\A/secret/([a-zA-Z0-9]+)\z})
150
+ match ? match[1] : value
151
+ rescue URI::InvalidURIError
152
+ value
153
+ end
154
+
155
+ def response_error_message(response)
156
+ return 'Could not complete request' if response.nil?
157
+
158
+ # Symbol lookups preserve behavior for plain Ruby hashes even though
159
+ # parsed API responses usually support indifferent access.
160
+ ['message', :message, 'error', :error, 'field', :field].each do |key|
161
+ value = response[key] if response.respond_to?(:[])
162
+ return value.to_s unless value.to_s.empty?
163
+ end
164
+ response.to_s
165
+ end
166
+
167
+ def secret_key_from_response(response)
168
+ response&.dig('record', 'secret', 'key')
169
+ end
170
+
171
+ def receipt_key_from_response(response)
172
+ response&.dig('record', 'receipt', 'key') || response&.dig('record', 'metadata', 'key')
173
+ end
174
+
175
+ def recipients_from_response(response)
176
+ recipients = response&.dig('record', 'receipt', 'recipients')
177
+ if recipients.nil? || recipients == '' || (recipients.is_a?(Array) && recipients.empty?)
178
+ recipients = response&.dig('details', 'recipient')
179
+ end
180
+ Array(recipients).flatten.compact.reject { |recipient| recipient.to_s.empty? }
181
+ end
182
+
183
+ private
184
+
185
+ def accepted_secret_host?(host, api_base_uri)
186
+ configured_host = URI.parse(api_base_uri.to_s).host
187
+ host == configured_host || host == 'onetimesecret.com' || host.end_with?('.onetimesecret.com')
188
+ rescue URI::InvalidURIError
189
+ host == 'onetimesecret.com' || host.end_with?('.onetimesecret.com')
190
+ end
131
191
  end
132
192
  end
133
193
  end
@@ -1,3 +1,3 @@
1
1
  module Onetime
2
- VERSION = "0.6.0"
2
+ VERSION = "0.6.2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: onetime-up
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.2
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: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: drydock
@@ -102,7 +102,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
102
102
  - !ruby/object:Gem::Version
103
103
  version: 2.0.0
104
104
  requirements: []
105
- rubygems_version: 3.6.7
105
+ rubygems_version: 4.0.7
106
106
  specification_version: 4
107
107
  summary: Command-line tool and library for onetimesecret.com API (API v2)
108
108
  test_files: []