konfidant 0.7.0 → 1.4.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: d76590436720d9998f06a04fffa4a5a06b3aff597c675e6b4a1eafe770cd383f
4
- data.tar.gz: 2e09ccdb885e047fdefe69faeba3289a919052534d07ef0bc5040a0e91256b14
3
+ metadata.gz: 2dd0f63c7378b463ca026143137aff868ad08b6e7d8b8a8672120fa283138326
4
+ data.tar.gz: 9b77783dc745eb9919f0bb864cd506899d2651b957af20da128639329797c5a2
5
5
  SHA512:
6
- metadata.gz: 6e858d0172471e5e88261696bc2201307e582f4003eb588ee6308fb0c1e2d280c72e6673b5329c00b2efc64b204a4abb9adacd848700d6a2daaeb04fe491a5c1
7
- data.tar.gz: 1ecd149e3f5efed0fb906b75499789610b5305dbb816b95dbad72074a022937a1f42126556faedd248d9a96e71a5c8e40d87fbb06e4cb2f4f1736cf443b9a691
6
+ metadata.gz: f704dc0b64fdb6820a0a9220f4bbb8abc876d402ee96bffdbac2b4187e53953528ae057661a57da014879ed2dfda51dda32dbc1684a25601b370ec8693d87037
7
+ data.tar.gz: 5f266d4494ae32900e035b0b75d23150441c9630f006fb6d22263ad7e4edf61736d59e793ea333b82953ffa217b74954dba2dc0ab4545c73aff52be249415571
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Konfidant
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -3,32 +3,38 @@
3
3
  [![Test](https://github.com/konfidant/sdk-ruby/actions/workflows/test.yml/badge.svg)](https://github.com/konfidant/sdk-ruby/actions/workflows/test.yml)
4
4
  [![Codacy Badge](https://app.codacy.com/project/badge/Grade/eb3798f7eb59412abc2bd3ce307760b5)](https://app.codacy.com/gh/konfidant/sdk-ruby/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
5
5
  [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/eb3798f7eb59412abc2bd3ce307760b5)](https://app.codacy.com/gh/konfidant/sdk-ruby/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
6
+ [![Quality gate status](https://sonarcloud.io/api/project_badges/measure?project=konfidant_sdk-ruby&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=konfidant_sdk-ruby)
6
7
 
7
- Official Ruby SDK for the [Konfidant](https://www.konfidant.app) API.
8
+ Official Ruby SDK for the [Konfidant](https://www.konfidant.app?utm_source=github&utm_medium=rubysdk&utm_campaign=github) API.
8
9
 
9
- Konfidant lets you share secrets — encrypted text and files — that self-destruct after being read.
10
+ Konfidant lets you share secrets — text and files — that self-destruct after being read.
11
+
12
+ ## Zero-knowledge model
13
+
14
+ All content is **encrypted on your machine** before anything is sent:
15
+
16
+ - Every share gets a fresh random 256-bit key. Text, file bytes, file name and MIME type are encrypted together
17
+ with AES-256-GCM in the chunked KNF1
18
+ format (stdlib `OpenSSL` only).
19
+ - Konfidant receives only ciphertext and its size. It never sees the key, the content or the file name.
20
+ - The key lives **only in the share link's URL fragment**: `https://download.konfidant.app/#t=<token>&k=<key>`.
21
+ Browsers never send the fragment to a server. The `t` token is single-use, so the ciphertext can be fetched once.
22
+ - Anyone holding the full `share_url` can read the secret once — treat it like the secret itself. If you lose it,
23
+ the content cannot be recovered by anyone, including Konfidant.
10
24
 
11
25
  ---
12
26
 
13
27
  ## Installation
14
28
 
15
- Add to your `Gemfile`:
16
-
17
29
  ```ruby
18
30
  gem 'konfidant'
19
31
  ```
20
32
 
21
- Then run:
22
-
23
33
  ```bash
24
- bundle install
34
+ bundle install # or: gem install konfidant
25
35
  ```
26
36
 
27
- Or install directly:
28
-
29
- ```bash
30
- gem install konfidant
31
- ```
37
+ Requires Ruby >= 3.2. No runtime dependencies (stdlib `net/http`, `openssl`, `json`).
32
38
 
33
39
  ---
34
40
 
@@ -37,307 +43,172 @@ gem install konfidant
37
43
  ```ruby
38
44
  require 'konfidant'
39
45
 
40
- client = Konfidant::Client.new(api_key: 'your-api-key')
41
-
42
- result = client.share_text(text: 'super-secret-password', ttl_hours: 24)
43
-
44
- puts "Share this link: #{result.share_url}"
45
- ```
46
-
47
- ---
48
-
49
- ## Authentication
46
+ client = Konfidant::Client.new(api_key: ENV['KONFIDANT_API_KEY'])
50
47
 
51
- All requests require a Bearer API key. Generate one from the [Konfidant dashboard](https://www.konfidant.app).
48
+ text = client.share_text(text: 'db-password: hunter2', ttl_hours: 24)
49
+ puts text.share_url
52
50
 
53
- ```ruby
54
- client = Konfidant::Client.new(api_key: ENV['KONFIDANT_API_KEY'])
51
+ file = File.open('contract.pdf', 'rb') do |f|
52
+ client.share_file(content: f, filename: 'contract.pdf', content_type: 'application/pdf', ttl_hours: 48)
53
+ end
54
+ puts file.share_url
55
55
  ```
56
56
 
57
57
  ---
58
58
 
59
- ## API Reference
59
+ ## API reference
60
60
 
61
- ### `Konfidant::Client.new`
61
+ ### `Konfidant::Client.new(api_key:, base_url: nil, http_timeout: 120)`
62
62
 
63
63
  | Option | Type | Required | Description |
64
64
  |----------------|-----------|----------|----------------------------------------------------------------------|
65
- | `api_key` | `String` | Yes | Your Konfidant API key |
66
- | `base_url` | `String` | No | Override the base URL (default: `https://www.konfidant.app`) |
65
+ | `api_key` | `String` | Yes | Your Konfidant API key (sent as `Authorization: Bearer …`) |
66
+ | `base_url` | `String` | No | Override the API base URL (default: `https://www.konfidant.app`) |
67
67
  | `http_timeout` | `Integer` | No | Per-request HTTP timeout in seconds (default: `120`; `nil` disables) |
68
68
 
69
69
  Raises `ArgumentError` if `api_key` is nil or empty.
70
70
 
71
- ---
72
-
73
- ### `client.share_text(text:, ttl_hours:)`
74
-
75
- Encrypt and share a text message.
76
-
77
- | Argument | Type | Description |
78
- |-------------|-----------|--------------------------|
79
- | `text` | `String` | The secret text to share |
80
- | `ttl_hours` | `Integer` | Time-to-live in hours |
81
-
82
- Returns a `Konfidant::ShareTextResponse`:
83
-
84
- | Field | Type | Description |
85
- |----------------|-----------|---------------------------------------------|
86
- | `text_id` | `String` | Unique ID of the shared text |
87
- | `share_url` | `String` | One-time download link to send to recipient |
88
- | `expires_at` | `String` | Expiry datetime |
89
- | `verified_burn`| `Boolean` | Whether burn-on-read is verified |
90
-
91
- #### Example: share text
92
-
93
- ```ruby
94
- result = client.share_text(text: 'db-password: hunter2', ttl_hours: 48)
95
-
96
- puts result.share_url # send to recipient
97
- puts result.expires_at
98
- puts result.verified_burn
99
- ```
100
-
101
- ---
102
-
103
- ### `client.share_file(filename:, file_size:, ttl_hours:)`
71
+ ### `client.share_text(text:, ttl_hours: nil)` → `Konfidant::TextShare`
104
72
 
105
- Request a presigned upload URL for a file. Use the returned response with `upload_file` to complete
106
- the upload, then poll `get_file_status` for the share link.
73
+ Encrypts `text` (UTF-8, no SDK-side size limit; your plan's limit applies server-side) and uploads the ciphertext.
74
+ `ttl_hours` is omitted from the request when `nil` (server default applies).
107
75
 
108
- > For a one-call convenience wrapper, see [`share_and_upload_file`](#clientshare_and_upload_file).
76
+ | Field | Type | Description |
77
+ |--------------|---------------|------------------------------------------------|
78
+ | `share_url` | `String` | One-time link **including the key** (`&k=…`) |
79
+ | `text_id` | `String, nil` | Text ID (`nil` when verified burn is disabled) |
80
+ | `expires_at` | `String` | ISO 8601 expiry |
109
81
 
110
- | Argument | Type | Description |
111
- |-------------|-----------|----------------------------------|
112
- | `filename` | `String` | Original filename with extension |
113
- | `file_size` | `Integer` | File size in bytes |
114
- | `ttl_hours` | `Integer` | Time-to-live in hours |
82
+ ### `client.share_file(content:, filename:, content_type: '', ttl_hours: nil)` → `Konfidant::FileShare`
115
83
 
116
- Returns a `Konfidant::ShareFileResponse`:
84
+ Encrypts and shares a file in one call: encrypt → `create_file_upload` → `upload_ciphertext` → `complete_file_upload`.
117
85
 
118
- | Field | Type | Description |
119
- |--------------------|-------------------------------|-------------------------------------------------|
120
- | `upload_url` | `String` | Short-lived presigned S3 PUT URL |
121
- | `file_key` | `String` | Use with `get_file_status` and `upload_file` |
122
- | `poll_url` | `String` | Convenience URL for status polling |
123
- | `metadata_headers` | `Konfidant::FileMetadataHeaders` | Required S3 headers — passed by `upload_file` |
86
+ | Argument | Type | Description |
87
+ |----------------|--------------|-------------------------------------------------------------------------------------|
88
+ | `content` | `String, IO` | File bytes, or a readable IO. `File`/`StringIO` are streamed chunk by chunk; other IOs are read into memory |
89
+ | `filename` | `String` | Original file name, at most 1 024 UTF-8 bytes (encrypted) |
90
+ | `content_type` | `String` | MIME type, at most 255 bytes, may be empty (encrypted) |
91
+ | `ttl_hours` | `Integer` | Time-to-live in hours (optional) |
124
92
 
125
- ---
126
-
127
- ### `client.upload_file(io:, size:, content_type:, presigned:)`
128
-
129
- Upload file bytes to the presigned S3 URL from `share_file`. Automatically attaches the required
130
- S3 metadata headers. Does **not** send the Konfidant `Authorization` header to S3.
93
+ | Field | Type | Description |
94
+ |-----------------|---------------|----------------------------------------------|
95
+ | `share_url` | `String` | One-time link **including the key** (`&k=…`) |
96
+ | `file_id` | `String, nil` | File ID (`nil` when verified burn is off) |
97
+ | `expires_at` | `String` | ISO 8601 expiry |
98
+ | `verified_burn` | `Boolean` | Whether verified burn is enabled |
131
99
 
132
- | Argument | Type | Description |
133
- |----------------|-----------------------------|---------------------------------------|
134
- | `io` | `IO` | Readable IO object (File, StringIO) |
135
- | `size` | `Integer` | Content-Length in bytes |
136
- | `content_type` | `String` | MIME type (e.g. `application/pdf`) |
137
- | `presigned` | `Konfidant::ShareFileResponse` | Full response from `share_file` |
100
+ ### Low-level file flow
138
101
 
139
- Returns `nil` on success. Raises `Konfidant::ApiError` on S3 error.
140
-
141
- #### Example: manual three-step flow
102
+ Use these when you need control over each step (e.g. your own retry logic).
142
103
 
143
104
  ```ruby
144
- file = File.open('report.pdf', 'rb')
145
- size = File.size('report.pdf')
146
-
147
- # Step 1 – get presigned URL
148
- presigned = client.share_file(
149
- filename: 'report.pdf',
150
- file_size: size,
151
- ttl_hours: 72
152
- )
153
-
154
- # Step 2 – upload to S3
155
- client.upload_file(
156
- io: file,
157
- size: size,
158
- content_type: 'application/pdf',
159
- presigned: presigned
160
- )
161
-
162
- # Step 3 – poll for share link
163
- loop do
164
- status = client.get_file_status(presigned.file_key)
165
- if status.status == 'complete'
166
- puts "Share URL: #{status.share_url}"
167
- break
168
- end
169
- sleep 2
170
- end
171
- ```
172
-
173
- ---
174
-
175
- ### `client.get_file_status(file_key)`
176
-
177
- Poll the encryption status of an uploaded file.
178
-
179
- | Argument | Type | Description |
180
- |------------|----------|-----------------------------------------------|
181
- | `file_key` | `String` | The `file_key` from the `share_file` response |
182
-
183
- Returns a `Konfidant::FileStatusResponse`:
184
-
185
- | Field | Type | When set |
186
- |----------------|-----------|-----------------------------------------|
187
- | `status` | `String` | Always (`"processing"` or `"complete"`) |
188
- | `message` | `String` | When `processing` |
189
- | `file_id` | `String` | When `complete` |
190
- | `file_name` | `String` | When `complete` |
191
- | `share_url` | `String` | When `complete` |
192
- | `expires_at` | `String` | When `complete` |
193
- | `verified_burn`| `Boolean` | When `complete` |
194
-
195
- ---
196
-
197
- ### `client.list_shares(type: nil, status: nil, limit: nil, offset: nil)`
198
-
199
- List all shares for the authenticated organization. All parameters are optional.
105
+ key = Konfidant::Knf.generate_key
106
+ encryptor = Konfidant::Knf.encryptor(key: key, content: File.open('a.zip', 'rb'), kind: 'file',
107
+ name: 'a.zip', mime: 'application/zip')
200
108
 
201
- | Argument | Type | Description |
202
- |----------|----------|--------------------------------|
203
- | `type` | `String` | `"file"` or `"text"` |
204
- | `status` | `String` | `"active"` or `"accessed"` |
205
- | `limit` | `Integer`| Page size (default 50) |
206
- | `offset` | `Integer`| Pagination offset |
109
+ upload = client.create_file_upload(ciphertext_size: encryptor.size, ttl_hours: 24)
110
+ client.upload_ciphertext(upload: upload, ciphertext: encryptor) # String or IO-like
111
+ done = client.complete_file_upload(file_key: upload.file_key)
207
112
 
208
- Returns a `Konfidant::ListSharesResponse`:
209
-
210
- | Field | Type | Description |
211
- |--------------|----------------------------|---------------------|
212
- | `shares` | `Array<Konfidant::Share>` | Share entries |
213
- | `pagination` | `Konfidant::Pagination` | Page metadata |
214
-
215
- Each `Konfidant::Share`:
216
-
217
- | Field | Type | Description |
218
- |------------------|---------------|-------------------------|
219
- | `type` | `String` | `"file"` or `"text"` |
220
- | `file_name` | `String` | Filename or text label |
221
- | `file_size_bytes`| `Integer` | Size in bytes |
222
- | `created_at` | `String` | Creation datetime |
223
- | `expires_at` | `String` | Expiry datetime |
224
- | `accessed_at` | `String, nil` | Access datetime, or nil |
225
- | `created_by` | `String` | Email of creator |
226
-
227
- `Konfidant::Pagination`:
113
+ share_url = Konfidant::Knf.build_share_url(done.download_url, key)
114
+ ```
228
115
 
229
- | Field | Type | Description |
230
- |------------|-----------|--------------------------|
231
- | `total` | `Integer` | Total number of shares |
232
- | `limit` | `Integer` | Page size used |
233
- | `offset` | `Integer` | Offset used |
234
- | `has_more` | `Boolean` | Whether more pages exist |
116
+ | Method | Request | Returns |
117
+ |-----------------------------------------------------|--------------------------------------------------|-----------------------------------------------------------------------------------------|
118
+ | `create_file_upload(ciphertext_size:, ttl_hours:)` | `POST /api/v1/files` | `FileUpload(upload_url, file_key, upload_headers, upload_expires_in, ciphertext_size)` |
119
+ | `upload_ciphertext(upload:, ciphertext:)` | `PUT upload_url` with exactly `upload_headers` | `nil`; never sends the API key |
120
+ | `complete_file_upload(file_key:)` | `POST /api/v1/files/{file_key}/complete` | `CompletedUpload(download_url, file_id, expires_at, verified_burn)` |
235
121
 
236
- #### Example: list shares
122
+ `complete_file_upload` raises `ApiError` with status `409` and message `upload_incomplete` if the PUT did not land.
123
+ `download_url` contains only the token — append the key with `Knf.build_share_url` before sending it to anyone.
237
124
 
238
- ```ruby
239
- result = client.list_shares(type: 'file', limit: 10)
125
+ ### `client.open_share(share_url)` → `Konfidant::OpenedShare`
240
126
 
241
- result.shares.each do |share|
242
- puts "#{share.file_name} — expires #{share.expires_at}"
243
- end
127
+ Recipient side. Sends only the token (`POST https://<link host>/api/download`, no API key, no decryption key),
128
+ receives the ciphertext and decrypts it locally. Opening a share **consumes it**.
244
129
 
245
- puts "Total: #{result.pagination.total}"
246
- puts "More? #{result.pagination.has_more}"
247
- ```
130
+ | Field | Type | Description |
131
+ |--------|---------------|---------------------------------------------|
132
+ | `kind` | `String` | `"text"` or `"file"` |
133
+ | `name` | `String` | File name (`""` for text) |
134
+ | `mime` | `String` | MIME type (`""` for text) |
135
+ | `data` | `String` | Decrypted bytes (binary / ASCII-8BIT) |
136
+ | `text` | `String, nil` | UTF-8 text for text shares, otherwise `nil` |
248
137
 
249
- ---
138
+ Raises `ApiError` (`410`) if the share was already opened or has expired, `KnfError` if the ciphertext does not
139
+ authenticate (wrong key, modified or truncated data).
250
140
 
251
- ### `client.share_and_upload_file`
141
+ ### `client.list_shares(type: nil, status: nil, limit: nil, offset: nil)` → `Konfidant::ListSharesResponse`
252
142
 
253
- Convenience wrapper that calls `share_file` → `upload_file` → polls `get_file_status` until complete.
143
+ | Argument | Type | Description |
144
+ |----------|-----------|----------------------------|
145
+ | `type` | `String` | `"file"` or `"text"` |
146
+ | `status` | `String` | `"active"` or `"accessed"` |
147
+ | `limit` | `Integer` | Page size (default 50) |
148
+ | `offset` | `Integer` | Pagination offset |
254
149
 
255
- ```ruby
256
- client.share_and_upload_file(
257
- io: io,
258
- size: size,
259
- filename: filename,
260
- content_type: content_type,
261
- ttl_hours: ttl_hours,
262
- poll_interval: 2, # seconds between status checks (default: 2)
263
- timeout: 60 # max wait for encryption in seconds (default: 60)
264
- )
265
- ```
150
+ Returns `shares` (`Array<Konfidant::Share>` with `type`, `file_size_bytes`, `created_at`, `expires_at`,
151
+ `accessed_at`, `created_by`) and `pagination` (`total`, `limit`, `offset`, `has_more`). File names are not
152
+ available: the server never sees them.
266
153
 
267
- | Argument | Type | Default | Description |
268
- |-----------------|-----------|---------|-------------------------------------|
269
- | `io` | `IO` | — | Readable IO object (File, StringIO) |
270
- | `size` | `Integer` | — | File size in bytes |
271
- | `filename` | `String` | — | Filename with extension |
272
- | `content_type` | `String` | — | MIME type |
273
- | `ttl_hours` | `Integer` | — | Time-to-live in hours |
274
- | `poll_interval` | `Numeric` | `2` | Seconds between status checks |
275
- | `timeout` | `Numeric` | `60` | Max seconds to wait for encryption |
154
+ ### `client.me` → `Konfidant::KeyInfo`
276
155
 
277
- Returns a `Konfidant::ShareResult`:
156
+ Describes the API key without creating a share: `organization` (`id`, `name`), `api_key` (`name`, `environment`,
157
+ `scopes`, `rate_limit_per_minute`, `expires_at`) and the plan's `limits` (`max_ttl_hours`, `max_file_size_mb`). It
158
+ needs no scope and does not count as an API share. Use it to check a key before storing it.
278
159
 
279
- | Field | Type | Description |
280
- |----------------|-----------|--------------------------------|
281
- | `share_url` | `String` | One-time download link |
282
- | `file_id` | `String` | Unique file ID |
283
- | `expires_at` | `String` | Expiry datetime |
284
- | `verified_burn`| `Boolean` | Whether burn-on-read is active |
160
+ ### `Konfidant::Knf`
285
161
 
286
- Raises `RuntimeError` with `"konfidant: encryption timed out after Ns"` if encryption does not complete
287
- within `timeout` seconds.
162
+ The KNF1 implementation is public for advanced use and interoperability:
288
163
 
289
- #### Example: share and upload file
164
+ | Method | Description |
165
+ |------------------------------------------------------------------------|-----------------------------------------------------|
166
+ | `generate_key` / `encode_key(key)` / `decode_key(str)` | 32-byte key; unpadded base64url (43 chars) |
167
+ | `encrypt(key:, content:, kind:, name: '', mime: '')` | Full ciphertext as a binary String |
168
+ | `encryptor(...)` | Streaming IO-like encryptor (`#read`, `#size`) |
169
+ | `encrypt_text(key:, text:)` | Text share ciphertext |
170
+ | `decrypt(key:, ciphertext:)` / `Decryptor.new(key:)#push`/`#finish` | Verify and decrypt (one-shot or streaming) |
171
+ | `ciphertext_size(metadata_length:, content_length:)` | Exact ciphertext length |
172
+ | `build_share_url(download_url, key)` / `parse_share_url(url)` | Share link helpers |
290
173
 
291
- ```ruby
292
- file = File.open('confidential.zip', 'rb')
293
- size = File.size('confidential.zip')
294
-
295
- result = client.share_and_upload_file(
296
- io: file,
297
- size: size,
298
- filename: 'confidential.zip',
299
- content_type: 'application/zip',
300
- ttl_hours: 48
301
- )
302
-
303
- puts "Ready to share: #{result.share_url}"
304
- ```
174
+ `encrypt` also accepts `chunk_size:` and a `nonce_prefix:` option that exists **only** for reproducing test
175
+ vectors — never pass `nonce_prefix` in production.
305
176
 
306
177
  ---
307
178
 
308
179
  ## Error handling
309
180
 
310
- All API and S3 errors raise `Konfidant::ApiError`.
181
+ All SDK errors inherit from `Konfidant::Error`.
311
182
 
312
183
  ```ruby
313
184
  begin
314
185
  client.share_text(text: 'secret', ttl_hours: 1)
315
186
  rescue Konfidant::ApiError => e
316
- puts e.message # e.g. "Missing or invalid Authorization header."
317
- puts e.status_code # e.g. 401
318
- puts e.body.inspect # parsed response body (Hash or String)
187
+ e.message # the "error" field of the JSON body, e.g. "upload_incomplete", or "HTTP 500"
188
+ e.status_code # e.g. 401
189
+ e.body # parsed body (Hash) or raw String
190
+ rescue Konfidant::KnfError => e
191
+ e.message # encryption/decryption failure
319
192
  end
320
193
  ```
321
194
 
322
- ### Common error codes
323
-
324
- | Status | Meaning |
325
- |--------|----------------------------|
326
- | `400` | Bad request / invalid body |
327
- | `401` | Missing or invalid API key |
328
- | `403` | Insufficient API key scope |
329
- | `404` | Resource not found |
195
+ | Status | Meaning |
196
+ |--------|-------------------------------------------|
197
+ | `400` | Bad request / invalid body |
198
+ | `401` | Missing or invalid API key |
199
+ | `403` | Insufficient API key scope |
200
+ | `404` | Resource not found |
201
+ | `409` | `upload_incomplete` on complete |
202
+ | `410` | Share already opened or expired |
330
203
 
331
204
  ---
332
205
 
333
206
  ## Development
334
207
 
335
208
  ```bash
336
- bundle install # install dependencies
337
- bundle exec rspec # run tests
209
+ bundle install
210
+ bundle exec rspec
338
211
  ```
339
212
 
340
- ### Requirements
341
-
342
- - Ruby >= 3.2.0
343
- - No runtime dependencies (uses stdlib `net/http`, `uri`, `json`)
213
+ `spec/fixtures/knf1-test-vectors.json` holds the cross-SDK KNF1 test vectors; the specs reproduce every ciphertext
214
+ byte for byte.