onetime-up 0.6.1 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2365f0f0c13cd862a91ae999e1ba0da6c17fcf764d523e8e1c53571709e12025
4
- data.tar.gz: 91f6e4b782395f53047321174913a5719a05c1a0af2d5336897a4a90e47da6b5
3
+ metadata.gz: 608460e67b5939792b49aad0117baa385911125337337624113f4e1a95388063
4
+ data.tar.gz: ff189cbd5cd36d19e102dea6dcd16acc2015a7de13400b5c42bd2638152e4533
5
5
  SHA512:
6
- metadata.gz: af94a360f15d01c1e70ec0284d9ffe31ec709012f7f5f2af5b3e618de0ad0b6533fa4b2aa07aecaf384fce3bc5cf2c2f60fead9d36670359e19e2309fc1a0862
7
- data.tar.gz: af55d8f2c1b2b72aaa933dd5231bbcb0b11972b13c4d6190889feca72e78010e0556ae93ea3d13466a4e06a5c59fd043c07a2c03d1a4bdf7d06973e114a7b750
6
+ metadata.gz: dfd25117746474d2ccb452aaeeaa5dcbb4b8ebe3c4a28c078ed11f33c2011bb19d60a25058be2a1fa5e9f98a9e56da75a4a5c877a7c559b6305f40753f38c031
7
+ data.tar.gz: 66b1255b1dd1d9f8c18e7e617daea8034ad518a0391f230276cb93a919a054a733fd2d2040ce3086c012ac29fc0dc76a824578e5e2f27acc2f584f2cbe3bfbf2
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'
@@ -79,7 +79,7 @@ class Onetime::CLI
79
79
  if @res.nil?
80
80
  raise RuntimeError, 'Could not complete request'
81
81
  elsif @api.response.code != 200
82
- raise RuntimeError, @res['message']
82
+ raise RuntimeError, OT::API.response_error_message(@res)
83
83
  end
84
84
  case obj.global.format
85
85
  when 'json'
@@ -104,15 +104,12 @@ class Onetime::CLI
104
104
 
105
105
  opts = { continue: true }
106
106
  opts[:passphrase] = obj.option.passphrase if obj.option.passphrase
107
- base_uri = URI.parse Onetime::API.base_uri
108
- if secret_key =~ /#{base_uri.hostname}\/secret\/([a-zA-Z0-9]+)/
109
- secret_key = $1
110
- end
107
+ secret_key = OT::API.extract_secret_key(secret_key)
111
108
  @res = @api.post '/secret/%s/reveal' % [secret_key], opts
112
109
  if @res.nil?
113
110
  raise RuntimeError, 'Could not complete request'
114
111
  elsif @api.response.code != 200
115
- raise RuntimeError, @res['message']
112
+ raise RuntimeError, OT::API.response_error_message(@res)
116
113
  end
117
114
  case obj.global.format
118
115
  when 'json'
@@ -154,9 +151,10 @@ class Onetime::CLI
154
151
  if @res.nil?
155
152
  raise RuntimeError, 'Could not complete request'
156
153
  elsif @api.response.code != 200
157
- raise RuntimeError, @res['message']
154
+ raise RuntimeError, OT::API.response_error_message(@res)
158
155
  end
159
- 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
160
158
  uri = OT::API.web_uri('secret', secret_key)
161
159
  case obj.global.format
162
160
  when 'json'
@@ -164,9 +162,9 @@ class Onetime::CLI
164
162
  when 'yaml'
165
163
  puts @res.to_yaml
166
164
  else
167
- recipient = @res['details']['recipient']
168
- if recipient && !recipient.compact.empty?
169
- 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(',')
170
168
  else
171
169
  puts uri
172
170
  end
@@ -179,6 +177,13 @@ class Onetime::CLI
179
177
  option :p, :passphrase, String, "Passphrase to encrypt the secret (something only you and recipient know)"
180
178
  option :r, :recipient, Array, "Email address to deliver the secret link"
181
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
182
187
  recipients = [obj.option.recipient, obj.global.recipient].flatten.compact.uniq
183
188
  opts = { :ttl => obj.option.ttl, :recipient => recipients }
184
189
  opts[:passphrase] = obj.option.passphrase if obj.option.passphrase
@@ -186,9 +191,10 @@ class Onetime::CLI
186
191
  if @res.nil?
187
192
  raise RuntimeError, 'Could not complete request'
188
193
  elsif @api.response.code != 200
189
- raise RuntimeError, @res['message']
194
+ raise RuntimeError, OT::API.response_error_message(@res)
190
195
  end
191
- secret_key = @res['record']['secret']['key']
196
+ secret_key = OT::API.secret_key_from_response(@res)
197
+ raise RuntimeError, 'Unexpected response: missing record.secret.key' unless secret_key
192
198
  uri = OT::API.web_uri('secret', secret_key)
193
199
  case obj.global.format
194
200
  when 'json'
@@ -198,9 +204,9 @@ class Onetime::CLI
198
204
  when 'csv'
199
205
  puts uri
200
206
  else
201
- recipient = @res['details']['recipient']
202
- if recipient && !recipient.compact.empty?
203
- STDERR.puts '# Secret link sent to: %s' % recipient.join(',')
207
+ recipients = OT::API.recipients_from_response(@res)
208
+ if !recipients.empty?
209
+ STDERR.puts '# Secret link sent to: %s' % recipients.join(',')
204
210
  else
205
211
  puts uri
206
212
  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.1"
2
+ VERSION = "0.7.0"
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.1
4
+ version: 0.7.0
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-01-19 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.9
106
106
  specification_version: 4
107
107
  summary: Command-line tool and library for onetimesecret.com API (API v2)
108
108
  test_files: []