verifiabl-issuer 0.1.0.pre.rc.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 92288131fb3b5bf6cc2533bf0eb1c8f7a0d351ba5256fe98a7a66a8a40992bcc
4
+ data.tar.gz: 3cbe2c9fade1351a663c98578ec719161b39d0201ed21839d9ebb8181335fb07
5
+ SHA512:
6
+ metadata.gz: 2d390c9bf09d86a4675690d8d9af78def89063fcb7b36ff2e75ea53ef1fb3be4a4e6b3ae7b6f7db9733e49441893e48257b79a720d24c295cd35da47ebabb62e
7
+ data.tar.gz: 23f6605d84dce4cd2f10a6da1228183589ee37b45a8e60759aa5094d6a4cadf147bb78e0a6b26867120d45f4a03b63c5f49874ff1b14ee6856d0d307d5584817
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ All notable changes to the Verifiabl Ruby SDK will be documented in this file.
4
+
5
+ ## Unreleased
6
+
7
+ ## [0.1.0-rc.1] - 2026-09-17
8
+
9
+ - Add the official Ruby issuer client for single and batch payslip registration, with OAuth token caching, safe retries, request deadlines, validation, and observability hooks.
10
+ - Add compatible PII encryption and Verifiabl v2 QR generation with SVG and PNG rendering using the official frame assets.
11
+ - Add runnable API-managed and self-managed issuance examples.
12
+ - Support Ruby 3.3 through Ruby 4.0 with checked RBS signatures.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Verifiabl
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 ADDED
@@ -0,0 +1,347 @@
1
+ # Verifiabl Ruby SDK
2
+
3
+ Official Ruby SDK for issuing Verifiabl payslip QR codes. It includes offline protocol primitives,
4
+ cross-SDK-compatible QR generation, deterministic SVG/PNG rendering, a hardened OAuth issuer
5
+ client, RBS signatures, and packed-gem consumer qualification.
6
+
7
+ ## Installation
8
+
9
+ Add the gem to your bundle:
10
+
11
+ ```ruby
12
+ gem "verifiabl-issuer"
13
+ ```
14
+
15
+ Then run `bundle install`. Bundler loads the gem through its package-name entry point:
16
+
17
+ ```ruby
18
+ require "verifiabl-issuer"
19
+ ```
20
+
21
+ That entry point is a compatibility shim and loads the canonical namespaced entry point. Applications
22
+ that require dependencies explicitly can use this path instead:
23
+
24
+ ```ruby
25
+ require "verifiabl/issuer"
26
+ ```
27
+
28
+ Both forms load the same `Verifiabl::Issuer` implementation; applications do not need to require
29
+ both. Ruby 3.3 or newer is required. QR encoding uses the pure-Ruby `rqrcode_core` gem; Verifiabl
30
+ owns canonical mask selection and badge rendering rather than depending on the gem's PNG or SVG
31
+ exporters.
32
+
33
+ ## Configuration
34
+
35
+ Construct and retain an issuer client explicitly:
36
+
37
+ ```ruby
38
+ issuer = Verifiabl::Issuer::Client.new(
39
+ Verifiabl::Issuer::Configuration.new(
40
+ environment: :sandbox,
41
+ client_id: ENV.fetch("VERIFIABL_CLIENT_ID"),
42
+ client_secret: ENV.fetch("VERIFIABL_CLIENT_SECRET")
43
+ )
44
+ )
45
+ ```
46
+
47
+ The SDK is framework-neutral: it does not load Rails or Active Support, install framework hooks, or
48
+ configure logging. Applications own their long-lived client and place it in their normal dependency
49
+ container; Rails applications commonly construct it in an initializer. A client is safe to share
50
+ between threads and refreshes its OAuth cache after a process fork.
51
+
52
+ Set `on_request`, `on_response`, and `on_error` on `Configuration` when request telemetry is needed.
53
+ Events contain request metadata but never request bodies, credentials, or payslip data. Observer
54
+ failures do not change request behavior.
55
+
56
+ The default overall deadline is 30 seconds and includes OAuth, 401 refresh, retry attempts, and
57
+ backoff. The Verifiabl reference is the idempotency key. `register_non_pii` generates and sends one
58
+ when omitted, so it and batch registration can safely retry transport failures, 408s, 429s, and 5xx
59
+ responses. API-managed barcode registration uses a server-generated reference and retries only 429,
60
+ which is rejected before processing. Configure these limits when needed:
61
+
62
+ ```ruby
63
+ configuration = Verifiabl::Issuer::Configuration.new(
64
+ client_id: ENV.fetch("VERIFIABL_CLIENT_ID"),
65
+ client_secret: ENV.fetch("VERIFIABL_CLIENT_SECRET"),
66
+ timeout: 10,
67
+ max_retries: 2
68
+ )
69
+ issuer = Verifiabl::Issuer::Client.new(configuration)
70
+ ```
71
+
72
+ Endpoint overrides are intended only for development. Issuer overrides require HTTPS except for
73
+ loopback HTTP. OAuth overrides are restricted to Verifiabl auth hosts or loopback addresses.
74
+
75
+ ## Issue payslips
76
+
77
+ Choose one barcode flow for each payslip. Do not call both registration methods for the same
78
+ issuance.
79
+
80
+ ### Self-managed barcode flow
81
+
82
+ Use this flow when your application renders the barcode. The following example is generated from
83
+ the runnable
84
+ [`examples/issuer/basic/issue_payslips_self_managed.rb`](../../examples/issuer/basic/issue_payslips_self_managed.rb)
85
+ source.
86
+
87
+ <!-- snippet:ruby.basic-issuance:start -->
88
+
89
+ ```ruby
90
+ require "base64"
91
+ require "fileutils"
92
+ require "verifiabl/issuer"
93
+
94
+ PAYSLIP = {
95
+ external_id: "PAY-1001",
96
+ pii: {
97
+ employee_name: "Jane A. Doe",
98
+ position: "Senior Developer",
99
+ department: "Engineering",
100
+ employer_abn: "12345678901",
101
+ bsb: "062-000",
102
+ account_number: "12345678",
103
+ account_name: "Jane A Doe",
104
+ address: "12 Example St, Sydney NSW 2000"
105
+ },
106
+ non_pii: {
107
+ period_start: "2026-08-01",
108
+ period_end: "2026-08-31",
109
+ payment_date: "2026-09-04",
110
+ currency: "AUD",
111
+ gross_cents: 900_000,
112
+ paygw_cents: 225_000,
113
+ net_cents: 675_000,
114
+ ytd_gross_cents: 5_400_000,
115
+ ytd_paygw_cents: 1_350_000
116
+ }
117
+ }.freeze
118
+
119
+ issuer = Verifiabl::Issuer::Client.new(
120
+ Verifiabl::Issuer::Configuration.new(
121
+ environment: :sandbox,
122
+ client_id: ENV.fetch("VERIFIABL_CLIENT_ID"),
123
+ client_secret: ENV.fetch("VERIFIABL_CLIENT_SECRET")
124
+ )
125
+ )
126
+
127
+ provider_encryption_key = Base64.strict_decode64(
128
+ ENV.fetch("VERIFIABL_ENCRYPTION_KEY_BASE64")
129
+ )
130
+ plaintext = Verifiabl::Issuer.format_pii(PAYSLIP.fetch(:pii))
131
+
132
+ encrypted = Verifiabl::Issuer.encrypt_pii(plaintext, provider_encryption_key)
133
+
134
+ registration = {
135
+ schema: "au.payslip.v1",
136
+ issued_at: Time.now.utc,
137
+ payslip_non_pii: PAYSLIP.fetch(:non_pii),
138
+ encryption_metadata: encrypted.encryption_metadata
139
+ }
140
+
141
+ result = issuer.register_non_pii(**registration)
142
+
143
+ barcode = Verifiabl::Issuer.build_barcode_svg(
144
+ verifiabl_reference: result.verifiabl_reference,
145
+ encrypted_pii: encrypted.encrypted_pii,
146
+ environment: :sandbox
147
+ )
148
+
149
+ output_path = File.expand_path("output/self-managed-barcode.svg", __dir__)
150
+ FileUtils.mkdir_p(File.dirname(output_path))
151
+
152
+ File.write(output_path, barcode.svg)
153
+ puts "Registered #{result.verifiabl_reference} and wrote #{output_path}"
154
+ ```
155
+
156
+ <!-- snippet:ruby.basic-issuance:end -->
157
+
158
+ `register_non_pii` sends the non-PII fields and encryption metadata, but not the encrypted PII. The
159
+ application combines the returned reference with the encrypted PII to render the SVG locally.
160
+
161
+ ### API-managed barcode flow
162
+
163
+ Use this alternative when Verifiabl should render the barcode PNG. The following example is
164
+ generated from the runnable
165
+ [`examples/issuer/basic/issue_payslips_api_managed.rb`](../../examples/issuer/basic/issue_payslips_api_managed.rb)
166
+ source.
167
+
168
+ <!-- snippet:ruby.api-managed-issuance:start -->
169
+
170
+ ```ruby
171
+ require "base64"
172
+ require "fileutils"
173
+ require "verifiabl/issuer"
174
+
175
+ PAYSLIP = {
176
+ external_id: "PAY-1001",
177
+ pii: {
178
+ employee_name: "Jane A. Doe",
179
+ position: "Senior Developer",
180
+ department: "Engineering",
181
+ employer_abn: "12345678901",
182
+ bsb: "062-000",
183
+ account_number: "12345678",
184
+ account_name: "Jane A Doe",
185
+ address: "12 Example St, Sydney NSW 2000"
186
+ },
187
+ non_pii: {
188
+ period_start: "2026-08-01",
189
+ period_end: "2026-08-31",
190
+ payment_date: "2026-09-04",
191
+ currency: "AUD",
192
+ gross_cents: 900_000,
193
+ paygw_cents: 225_000,
194
+ net_cents: 675_000,
195
+ ytd_gross_cents: 5_400_000,
196
+ ytd_paygw_cents: 1_350_000
197
+ }
198
+ }.freeze
199
+
200
+ issuer = Verifiabl::Issuer::Client.new(
201
+ Verifiabl::Issuer::Configuration.new(
202
+ environment: :sandbox,
203
+ client_id: ENV.fetch("VERIFIABL_CLIENT_ID"),
204
+ client_secret: ENV.fetch("VERIFIABL_CLIENT_SECRET")
205
+ )
206
+ )
207
+
208
+ provider_encryption_key = Base64.strict_decode64(
209
+ ENV.fetch("VERIFIABL_ENCRYPTION_KEY_BASE64")
210
+ )
211
+ plaintext = Verifiabl::Issuer.format_pii(PAYSLIP.fetch(:pii))
212
+
213
+ encrypted = Verifiabl::Issuer.encrypt_pii(plaintext, provider_encryption_key)
214
+
215
+ registration = {
216
+ schema: "au.payslip.v1",
217
+ issued_at: Time.now.utc,
218
+ payslip_non_pii: PAYSLIP.fetch(:non_pii),
219
+ encryption_metadata: encrypted.encryption_metadata
220
+ }
221
+
222
+ result = issuer.register_and_build_barcode(
223
+ encrypted_pii: encrypted.encrypted_pii,
224
+ **registration
225
+ )
226
+
227
+ output_path = File.expand_path("output/api-managed-barcode.png", __dir__)
228
+ FileUtils.mkdir_p(File.dirname(output_path))
229
+
230
+ File.binwrite(output_path, Base64.strict_decode64(result.barcode.data))
231
+ puts "Registered #{result.verifiabl_reference} and wrote #{output_path}"
232
+ ```
233
+
234
+ <!-- snippet:ruby.api-managed-issuance:end -->
235
+
236
+ `register_and_build_barcode` sends the encrypted PII in addition to the non-PII fields and encryption
237
+ metadata. Verifiabl uses the ciphertext to render the returned PNG and does not retain it. Plaintext
238
+ PII is never sent in either flow.
239
+
240
+ ### Retries and batches
241
+
242
+ For a self-managed registration that must remain retryable across process restarts, generate and
243
+ persist the reference before the first call, then reuse it on every attempt:
244
+
245
+ ```ruby
246
+ reference = Verifiabl::Issuer.generate_verifiabl_reference
247
+ persist_with_issuance_record(reference)
248
+ result = issuer.register_non_pii(
249
+ verifiabl_reference: reference,
250
+ **registration
251
+ )
252
+ ```
253
+
254
+ For pay runs, register up to 1,000 self-managed records in one request:
255
+
256
+ ```ruby
257
+ batch = issuer.register_non_pii_batch(records)
258
+ batch.results.each do |item|
259
+ case item.status
260
+ when "created", "duplicate"
261
+ puts item.verifiabl_reference
262
+ when "error"
263
+ warn "#{item.external_id}: #{item.code} #{item.detail}"
264
+ end
265
+ end
266
+ ```
267
+
268
+ The API returns `201` for the first registration and `200` for an identical replay. Reusing a
269
+ reference with different content raises `ApiError` with code `CONFLICT`.
270
+
271
+ Successful calls return immutable typed result objects under `Verifiabl::Issuer::Responses`.
272
+ Malformed successful API responses raise `TransportError`. Issuer HTTP errors raise `ApiError` (or
273
+ `IvReuseError` for `IV_REUSED`). Non-timeout OAuth connectivity failures and invalid or unsuccessful
274
+ token responses raise `AuthError`. Deadline expiry raises `TimeoutError`, while issuer network
275
+ failures raise `TransportError`. Request and error messages never include the submitted payslip body.
276
+
277
+ ## Offline protocol primitives
278
+
279
+ Format and encrypt PII locally. The plaintext must never be logged or sent to Verifiabl:
280
+
281
+ ```ruby
282
+ plaintext = Verifiabl::Issuer.format_pii(
283
+ employee_name: "Jane Doe",
284
+ employer_abn: "53004085616",
285
+ address: "12 Example St, Sydney NSW 2000"
286
+ )
287
+
288
+ encrypted = Verifiabl::Issuer.encrypt_pii(plaintext, provider_encryption_key)
289
+ reference = Verifiabl::Issuer.generate_verifiabl_reference
290
+
291
+ scan_url = Verifiabl::Issuer.build_scan_url(
292
+ verifiabl_reference: reference,
293
+ encrypted_pii: encrypted.encrypted_pii,
294
+ environment: :sandbox
295
+ )
296
+
297
+ xmp_payload = Verifiabl::Issuer.build_barcode_payload(
298
+ verifiabl_reference: reference,
299
+ encrypted_pii: encrypted.encrypted_pii
300
+ )
301
+
302
+ svg = Verifiabl::Issuer.build_barcode_svg(
303
+ verifiabl_reference: reference,
304
+ encrypted_pii: encrypted.encrypted_pii,
305
+ environment: :sandbox,
306
+ width: 480
307
+ )
308
+ File.write("barcode.svg", svg.svg)
309
+
310
+ png = Verifiabl::Issuer.build_barcode_png(
311
+ verifiabl_reference: reference,
312
+ encrypted_pii: encrypted.encrypted_pii,
313
+ environment: :sandbox,
314
+ width: 720
315
+ )
316
+ File.binwrite("barcode.png", png.png)
317
+ ```
318
+
319
+ The renderers use the same official vector frame, rounded finder geometry, and QR error-correction
320
+ ladder as the Node and .NET SDKs. SVG widths are continuously scalable from 480 upward. PNG frames
321
+ are pre-rasterised for deterministic cross-SDK output and therefore support only `480`, `720`,
322
+ `960`, and `1440` pixels. Use `max_error_correction: :q` for greater damage recovery; the default is
323
+ `:m`, and the renderer steps down only when necessary to preserve the three-pixel module floor.
324
+ Each result's `degraded` field (`svg.degraded` or `png.degraded` above) is true when the renderer
325
+ steps down from the requested ceiling or the modules fall below the ideal four-pixel size.
326
+
327
+ `provider_encryption_key` must be exactly 32 bytes and loaded from a KMS or secrets manager. Every
328
+ encryption call creates a fresh AES-256-GCM IV. Ciphertext, IV, and authentication tags are exposed
329
+ as binary Ruby strings (`Encoding::BINARY`) and can be persisted directly in binary database
330
+ columns. The SDK encodes them only at an external boundary: base64url for issuer API requests and
331
+ uppercase, unpadded RFC 4648 Base32 for v2 barcode and XMP output. PDF integrations should store `xmp_payload` under
332
+ XMP namespace `https://verifiabl.io/ns/`, property `payload`. QR v2 is encoded as an explicit byte
333
+ prefix and alphanumeric ciphertext segment. Matrix version, mask, and modules match the Node and
334
+ .NET SDKs.
335
+
336
+ ## Development
337
+
338
+ ```sh
339
+ bundle install
340
+ bundle exec rake
341
+ (cd gems/verifiabl-issuer && bundle exec gem build verifiabl-issuer.gemspec)
342
+ bundle exec ruby script/packed_gem_check.rb
343
+ ```
344
+
345
+ ## License
346
+
347
+ [MIT](./LICENSE)
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verifiabl
4
+ module Issuer
5
+ module Base32
6
+ ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
7
+
8
+ module_function
9
+
10
+ # Encodes bytes as canonical uppercase, unpadded RFC 4648 Base32.
11
+ def encode(input)
12
+ raise ArgumentError, "input must be a String" unless input.is_a?(String)
13
+
14
+ output = +""
15
+ accumulator = 0
16
+ bits = 0
17
+ input.b.each_byte { |byte| accumulator, bits = append_byte(output, accumulator, bits, byte) }
18
+ output << ALPHABET[(accumulator << (5 - bits)) & 0x1f] if bits.positive?
19
+ output
20
+ end
21
+
22
+ def append_byte(output, accumulator, bits, byte)
23
+ accumulator = (accumulator << 8) | byte
24
+ bits += 8
25
+ while bits >= 5
26
+ bits -= 5
27
+ output << ALPHABET[(accumulator >> bits) & 0x1f]
28
+ end
29
+ [accumulator & ((1 << bits) - 1), bits]
30
+ end
31
+ private_class_method :append_byte
32
+ end
33
+ end
34
+ end