shojiku 0.1.0-x86_64-linux

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: d86825fdae8fc7dccbb563e21959290f8753f59cd8f420af7bd409dde7b98500
4
+ data.tar.gz: b5b298be47d729631ed20fe951ddbb6029f92d4d6066c59aa38cd2faef1ed53c
5
+ SHA512:
6
+ metadata.gz: e27af49e536e8d94c556f997369e36ae430e9d5fa47f8cf10e91d014001651646ce2fc2464da2f84c9426666e2681a5a9b437ba65ac283ce40ad3493f23671d4
7
+ data.tar.gz: 1d85cc726209f3cc6f5d0b8468db35209caa5c9cb9f57278b56e83c4949e9ee893863593446b749cf81cb38e7bdd4866d3ff043ed50eb19bc57aad6ba6e38aea
data/README.md ADDED
@@ -0,0 +1,319 @@
1
+ # Shojiku for Ruby
2
+
3
+ Ruby bindings for [Shojiku](../../README.md) — a document engine that
4
+ turns a YAML template plus your data into a deterministic PDF, signs it,
5
+ and verifies it.
6
+
7
+ > **Unreleased.** The gem is written and gated but not published yet —
8
+ > all seven Shojiku SDKs publish together at v0.1.0. Until then, install
9
+ > from a repository clone (see [Building the engine
10
+ > library](#building-the-engine-library) below), or use the CLI or the
11
+ > Docker image — see the [quickstart](../../docs/quickstart.md).
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ bundle add shojiku
17
+ ```
18
+
19
+ Platform gems carry a prebuilt engine library, so there is no build step
20
+ on the supported platforms (Linux and macOS on x86-64 and arm64, Windows
21
+ on x86-64). The engine is loaded through `fiddle` from the standard
22
+ library rather than as a native extension, which means one binary serves
23
+ every supported Ruby — no recompile when you upgrade the interpreter.
24
+
25
+ ## Usage
26
+
27
+ ```ruby
28
+ require "shojiku"
29
+
30
+ client = Shojiku::Client.new(templates: "app/templates")
31
+
32
+ result = client.generate("receipt_ja", {
33
+ customer: { name: "Yamada Shoji K.K." },
34
+ items: [{ name: "Consulting", qty: 1, price: 120_000 }],
35
+ })
36
+
37
+ if result.success?
38
+ result.artifact.write("receipt.pdf")
39
+ else
40
+ result.failure.causes.each { |cause| warn cause.to_s }
41
+ end
42
+ ```
43
+
44
+ `params` is a Hash, or a String you already have — JSON *or* YAML, since
45
+ the engine parses either and a String is passed through verbatim. There
46
+ is no per-format method: format dispatch is the engine's.
47
+
48
+ To render one template in several locales, derive a client:
49
+
50
+ ```ruby
51
+ client.with_lang("ja-JP").generate("receipt_ja", params)
52
+ ```
53
+
54
+ A derived client shares the loaded engine — deriving re-opens nothing —
55
+ and the locale it names beats the one the client was built with.
56
+
57
+ ### Sources you already hold
58
+
59
+ `generate` resolves a template *name* against the configured root.
60
+ When the sources come from somewhere else — object storage, a database,
61
+ a heredoc — hand them over directly:
62
+
63
+ ```ruby
64
+ result = client.generate_source(
65
+ template: File.read("invoice.yml"), # source text, never a path
66
+ definitions: definitions_yaml, # optional
67
+ assets_dir: "app/templates/invoice", # optional; bundled images resolve here
68
+ params: params
69
+ )
70
+ ```
71
+
72
+ Fetching the bytes stays your application's act: this gem opens no
73
+ sockets and reads no template off disk here. `template:` is **source
74
+ text** — a value that looks like a path is a template that fails to
75
+ parse, not a file this gem will open — so the containment rules that
76
+ guard the name entrance are not being bypassed, they are simply not
77
+ involved. A deployment that wants to forbid this entrance entirely
78
+ declares [`strict:`](#locking-down-where-signable-input-comes-from).
79
+
80
+ An archived document goes back in the same way:
81
+
82
+ ```ruby
83
+ archived = client.artifact(File.binread("2026-invoices/0042.pdf"))
84
+ archived.verify(anchors: "ca.crt")
85
+ ```
86
+
87
+ Signing is a separate step over the rendered document, and verification
88
+ is a separate step over that:
89
+
90
+ ```ruby
91
+ signed = result.artifact.sign(
92
+ Shojiku::LocalPem.new(key: "signer.key", cert: "signer.crt")
93
+ )
94
+
95
+ checked = signed.artifact.verify(anchors: "ca.crt")
96
+ checked.report.valid? # every check this release performs passed
97
+ checked.report.not_checked # => [:revocation, :timestamp]
98
+ ```
99
+
100
+ **Read `not_checked` beside the verdict.** `valid?` means "nothing we
101
+ looked at was wrong", not "this document is trustworthy" — revocation
102
+ and timestamping are not checked in this release, and the report says so
103
+ on a passing verdict as well as a failing one. A document whose
104
+ signature does not verify comes back as a *failed* result that still
105
+ carries the full report.
106
+
107
+ ### Results, not exceptions
108
+
109
+ Nothing raises in the normal flow. Every operation returns a result you
110
+ query — `success?`, the artifact or report, and the engine's diagnostics
111
+ either way (a render that *worked* can still have warned about an
112
+ overflowing box). A failure carries a trace: which step failed, a stable
113
+ `kind`, the message, and the cause chain underneath it.
114
+
115
+ ```ruby
116
+ result.warnings.each { |d| logger.warn("#{d.code}: #{d.message}") }
117
+ result.failure.step # => :generate
118
+ result.failure.kind # => "template_not_found"
119
+ result.failure.causes # => [the failure, its cause, …]
120
+ ```
121
+
122
+ Diagnostics keep the engine's stable `code` and typed `args` untouched,
123
+ so an application that renders its own localized messages has everything
124
+ it needs. This gem never translates them.
125
+
126
+ Exceptions are reserved for what Ruby reserves them for: programmer
127
+ misuse (`Shojiku::UsageError`) and an environment with no engine in it
128
+ (`Shojiku::LibraryNotFound`).
129
+
130
+ For a script that would rather have a stack trace than a branch, there
131
+ is an opt-in unwrap:
132
+
133
+ ```ruby
134
+ client.generate("receipt_ja", params).artifact!.write("receipt.pdf")
135
+ ```
136
+
137
+ `artifact!` / `report!` raise `Shojiku::UnwrapError` (carrying the whole
138
+ failure) when the result failed. The ruling behind them is deliberate
139
+ and shared by every Shojiku SDK: **calling unwrap on a failed result is
140
+ programmer misuse** — a caller who has not checked `success?` is
141
+ asserting the operation worked. Application code that handles failure
142
+ keeps using `success?`.
143
+
144
+ ## Templates
145
+
146
+ Template names are **identifiers, never paths**. A name resolves against
147
+ the configured template root:
148
+
149
+ ```text
150
+ app/templates/
151
+ receipt_ja/
152
+ templates.yml (required)
153
+ definitions.yml (optional)
154
+ assets/ (optional — bundled images resolve against it)
155
+ ```
156
+
157
+ Names containing a path separator, a `..` segment, a drive letter, a UNC
158
+ prefix, a control character, or a reserved Windows device name (`CON`,
159
+ `NUL`, `COM1`, …) are refused — on **every** platform, not only the ones
160
+ where they would resolve, so a name that works in development works in
161
+ production. A name that resolves outside the root through a symlink is
162
+ refused after canonicalization.
163
+
164
+ Every one of those is a *failed result*: a hostile name is a fact about
165
+ the request, not a bug in your program. A name that is not a String at
166
+ all is the other thing, and raises.
167
+
168
+ ## Configuration
169
+
170
+ ```ruby
171
+ Shojiku::Client.new(
172
+ templates: "app/templates", # the template root
173
+ font_dirs: ["vendor/fonts"], # extra font-pack search directories
174
+ locale_dirs: ["vendor/locales"],
175
+ lang: "ja-JP", # overrides the template's own default
176
+ library: "/opt/lib/libshojiku_capi.so",
177
+ logger: Rails.logger, # optional; host events only (see Logging)
178
+ strict: true, # the input ceiling (see below)
179
+ providers: { invoice: … }, # signing providers, by name
180
+ env: true # consult SHOJIKU_* variables at all
181
+ )
182
+ ```
183
+
184
+ | Variable | What it sets |
185
+ | --- | --- |
186
+ | `SHOJIKU_TEMPLATE_ROOT` | the template root |
187
+ | `SHOJIKU_FONT_DIR` | font-pack directories (`PATH`-separated) |
188
+ | `SHOJIKU_LOCALE_DIR` | locale-pack directories (`PATH`-separated) |
189
+ | `SHOJIKU_LIBRARY` | the engine library to load |
190
+
191
+ `env: false` disables **all** of them at once, for an application that
192
+ wants its configuration to come from nowhere but its own code.
193
+
194
+ The same settings can be defaults for every client, in the idiom an
195
+ initializer expects:
196
+
197
+ ```ruby
198
+ Shojiku.configure do |config|
199
+ config.templates = "app/templates"
200
+ config.lang = "ja-JP"
201
+ config.logger = Rails.logger
202
+ end
203
+ ```
204
+
205
+ This sets defaults for the constructor; it does not add a precedence
206
+ layer. An explicit argument still wins, and a configured value still
207
+ wins over the environment — with `strict:` the one deliberate exception
208
+ (below). `Shojiku.reset_configuration!` drops the lot, which is what a
209
+ test suite wants between examples.
210
+
211
+ ### Locking down where signable input comes from
212
+
213
+ Once you sign what you render, template input is a security boundary:
214
+ whoever controls the bytes controls what gets signed. An operator can
215
+ declare a ceiling:
216
+
217
+ ```ruby
218
+ Shojiku.configure do |config|
219
+ config.strict = true
220
+ config.providers = { invoice: Shojiku::LocalPem.new(key: "signer.key", cert: "signer.crt") }
221
+ end
222
+
223
+ client.sign(document, :invoice) # by name; the key path is not here
224
+ ```
225
+
226
+ A strict client:
227
+
228
+ - refuses `generate_source`, so every document it signs came from the
229
+ template root and its containment rules;
230
+ - signs only a document it rendered from that root — bytes handed over
231
+ whole (`client.artifact`) and bytes laid out from a caller's own
232
+ template are the same trust class, and signing does not launder them;
233
+ - takes signing material only as the **name** of a provider registered
234
+ in configuration, so a key path never appears in request-handling code
235
+ and the material is loaded by one object.
236
+
237
+ **Verification is never restricted.** Verifying bytes of unknown
238
+ provenance is the point of verify, and a locked-down deployment is
239
+ exactly the one that has to check an archived document it did not
240
+ produce.
241
+
242
+ Refusals raise `Shojiku::UsageError` rather than returning a failed
243
+ result: strict disables an *entrance*, so calling it is the program
244
+ contradicting its own deployment, not a fact about a document — and a
245
+ failed result is something `if result.success?` can swallow. Strict is
246
+ also the one setting `Shojiku.configure` wins outright: a restriction an
247
+ operator declared must not be lifted by `Client.new(strict: false)`.
248
+
249
+ ### Logging
250
+
251
+ Optional, silent by default, and narrow on purpose:
252
+
253
+ ```ruby
254
+ Shojiku::Client.new(templates: "app/templates", logger: Rails.logger)
255
+ ```
256
+
257
+ Any object answering `debug` is accepted (this gem takes no logger
258
+ dependency). It reports what the *binding* did — which library it
259
+ loaded and why that one, the ABI revision it found, which lifecycle step
260
+ ran, for how long, and whether it worked. It never logs params, document
261
+ bytes, key material, a passphrase, or the engine's diagnostics: a log
262
+ line is the easiest way for a secret to leave a process, and the
263
+ diagnostics already belong to the result you are holding.
264
+
265
+ ### Threads
266
+
267
+ A render, a signature and a verification each release Ruby's global VM
268
+ lock for the duration of the call into the engine, so a long render does
269
+ not block other threads in the process. `Client` is safe to share across
270
+ threads, and the engine library itself may be called concurrently — the
271
+ same params produce the same bytes whether one thread is rendering or
272
+ eight are.
273
+
274
+ Explicit configuration wins for the template root and the pack
275
+ directories — what an application renders is the application's own
276
+ decision. `SHOJIKU_LIBRARY` is the one that goes the other way and beats
277
+ an explicit `library:`, because *where the engine lives* is a deployment
278
+ decision that has to be able to override application code. That is the
279
+ same order `SHOJIKU_BIN` has in the SDKs that shell out to the CLI.
280
+
281
+ Nothing in this gem downloads anything, at install time or at run time.
282
+
283
+ ## Building the engine library
284
+
285
+ Until the platform gems are published, build the library from a clone:
286
+
287
+ ```bash
288
+ make capi-lib
289
+ ```
290
+
291
+ It lands in `dist/capi/local/`; point `SHOJIKU_LIBRARY` at it, or pass
292
+ `library:` to the client.
293
+
294
+ ## Development
295
+
296
+ Every gate runs in a container — no Ruby toolchain needed locally:
297
+
298
+ ```bash
299
+ make verify:sdk:ruby
300
+ ```
301
+
302
+ `make test:sdk:ruby` and `make lint:sdk:ruby` are the faster slices.
303
+
304
+ ## Requirements
305
+
306
+ Ruby 3.3 or newer.
307
+
308
+ ## Documentation
309
+
310
+ - [Template reference](../../docs/engine/README.md) — how to write the
311
+ YAML the engine renders
312
+ - [SDK policy](../../docs/agents/sdk.md) — the lifecycle contract every
313
+ Shojiku SDK implements
314
+
315
+ ## License
316
+
317
+ Licensed under any of [Apache-2.0](../../LICENSE-APACHE),
318
+ [MIT](../../LICENSE-MIT), or [BSD-3-Clause](../../LICENSE-BSD), at your
319
+ option.
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shojiku
4
+ # A rendered (and possibly signed) document.
5
+ #
6
+ # The application sees bytes and metadata — never a layout-engine internal,
7
+ # and never a handle it has to free. Freeing is the binding's job and it is
8
+ # already done by the time this object exists.
9
+ class DocumentArtifact
10
+ # The PDF, as binary. Always `ASCII-8BIT`: PDF bytes are not text and
11
+ # tagging them as any character encoding is how a document gets corrupted
12
+ # by a well-meaning transcode on the way to disk.
13
+ attr_reader :bytes
14
+
15
+ # How many pages the engine laid out. `nil` for an artifact that was
16
+ # signed rather than rendered — signing appends a revision to bytes it
17
+ # never measured, and a zero there would read as "a document with no
18
+ # pages".
19
+ attr_reader :page_count
20
+
21
+ attr_reader :diagnostics
22
+
23
+ # Where this document came from, which is what a strict client signs on:
24
+ #
25
+ # * `:rendered` — laid out from a template the configured root resolved,
26
+ # * `:source` — laid out from template bytes the application supplied,
27
+ # * `:loaded` — bytes the application supplied whole ({Client#artifact}).
28
+ #
29
+ # Only the first is signable under a {Lockdown}: in the other two the
30
+ # provenance of what gets signed is the application's rather than the
31
+ # deployment's, which is the distinction strict exists to draw. Signing
32
+ # inherits the origin of what it signed — appending a revision does not
33
+ # launder where the document came from. Verification is never restricted.
34
+ attr_reader :origin
35
+
36
+ # `origin` defaults to the LEAST privileged value, not the most: every
37
+ # internal path states it explicitly, so the default only ever applies to
38
+ # an artifact somebody built by hand — which is bytes handed over whole,
39
+ # and must not become signable under a lockdown by omission.
40
+ def initialize(bytes:, diagnostics:, client:, page_count: nil, origin: :loaded)
41
+ @bytes = bytes
42
+ @page_count = page_count
43
+ @diagnostics = diagnostics
44
+ @client = client
45
+ @origin = origin
46
+ end
47
+
48
+ # Whether these bytes were handed over whole rather than laid out here.
49
+ def loaded?
50
+ @origin == :loaded
51
+ end
52
+
53
+ # Writes the document. Binary mode explicitly — a PDF contains NUL and
54
+ # every other byte value, and text mode would translate line endings on
55
+ # Windows.
56
+ def write(path)
57
+ File.binwrite(path, @bytes)
58
+ path
59
+ end
60
+
61
+ def size
62
+ @bytes.bytesize
63
+ end
64
+
65
+ # Signs this document, returning a {Result} carrying the signed artifact.
66
+ # The signed bytes begin with these bytes byte for byte: signing appends a
67
+ # revision, it never rewrites what was there.
68
+ def sign(provider)
69
+ @client.sign(self, provider)
70
+ end
71
+
72
+ # Verifies this document against caller-supplied trust anchors, returning
73
+ # a {Result} carrying a {VerificationReport}.
74
+ def verify(anchors: nil, anchors_pem: nil)
75
+ @client.verify(self, anchors: anchors, anchors_pem: anchors_pem)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Shojiku
6
+ # The entry point: a configured engine, and the sources to render with it.
7
+ #
8
+ # ```ruby
9
+ # client = Shojiku::Client.new(templates: "app/templates")
10
+ # result = client.generate("receipt_ja", customer: { name: "…" })
11
+ # result.artifact.write("receipt.pdf") if result.success?
12
+ # ```
13
+ #
14
+ # **Two entrances, deliberately.** {#generate} takes a template NAME and
15
+ # resolves it against the configured root, which is where the containment
16
+ # rules live. {#generate_source} takes the sources as BYTES the application
17
+ # already has — fetched from object storage, read out of a database, written
18
+ # inline — because fetching is the application's act and this gem downloads
19
+ # nothing. Root containment does not apply to bytes a caller supplied: there
20
+ # is no root to be contained by, which is exactly why a strict client
21
+ # refuses that entrance (see {Lockdown}).
22
+ #
23
+ # **Precedence, and its one deliberate asymmetry.** An explicit `templates:`
24
+ # beats {Shojiku.configure}, which beats `SHOJIKU_TEMPLATE_ROOT`; the pack
25
+ # directories resolve the same way. What an application renders is the
26
+ # application's own decision. An explicit `library:` is the other way round —
27
+ # `SHOJIKU_LIBRARY` beats it — because where the ENGINE lives is an
28
+ # operator's decision that has to be able to win over application code, the
29
+ # same order the subprocess SDKs give `SHOJIKU_BIN`. Passing `env: false`
30
+ # disables every one of those lookups at once. `strict:` is the one setting
31
+ # {Shojiku.configure} wins outright, for the reason in {Lockdown}.
32
+ class Client
33
+ ANCHOR_FORMS = "`anchors:` (paths) or `anchors_pem:` (bytes)"
34
+
35
+ def initialize(templates: nil, font_dirs: nil, locale_dirs: nil, lang: nil, library: nil,
36
+ logger: nil, strict: nil, providers: nil, env: nil)
37
+ @settings = Settings.new(
38
+ templates:, font_dirs:, locale_dirs:, lang:, library:, logger:, strict:, providers:, env:
39
+ )
40
+ @engine = Engine.new(@settings.library)
41
+ end
42
+
43
+ def template_root
44
+ @settings.template_root
45
+ end
46
+
47
+ # A copy of this client that renders in `lang`, for the call — or the
48
+ # request — that needs a locale other than this client's own. A
49
+ # multi-locale application renders one template in each buyer's locale
50
+ # without building (or configuring) a second client.
51
+ #
52
+ # **A derived client rather than a `lang:` keyword on {#generate}, and
53
+ # that is a Ruby spelling of a contract every SDK shares.** `generate`
54
+ # takes its params as a trailing Hash, so a keyword argument beside it
55
+ # would turn the natural `generate("receipt", customer: …)` into an
56
+ # `unknown keyword: :customer` error — Ruby resolves that ambiguity
57
+ # against the common case. The other six, whose params are an ordinary
58
+ # argument, express the same override as a per-call option; what they
59
+ # mirror is that a per-call locale beats the client-wide one, not the
60
+ # spelling.
61
+ #
62
+ # The opened library, the template root and the lockdown are shared:
63
+ # deriving a client re-opens nothing.
64
+ def with_lang(lang)
65
+ copy = dup
66
+ copy.settings = @settings.with_lang(lang)
67
+ copy
68
+ end
69
+
70
+ # What this build of the engine can do — its version, capability keys and
71
+ # builtin locales. Gate a feature on this rather than on a gem version.
72
+ #
73
+ # A plain Hash, deliberately. The payload is an append-only wire this SDK
74
+ # does not model, exactly as a diagnostic's typed `args` pass through
75
+ # untranslated: a typed value object would have to grow a field in seven
76
+ # languages every time the engine adds one, and an application reading a
77
+ # key its engine is too old to send already has to handle nil.
78
+ def engine_info
79
+ snapshot = @engine.engine_info
80
+ Outcome.guard!(snapshot)
81
+ JSON.parse(snapshot.json)
82
+ end
83
+
84
+ # Renders `name` with `params`, returning a {Result}.
85
+ #
86
+ # `params` may be a Hash (serialized here) or a String you already have —
87
+ # JSON or YAML, since the engine parses either and a String is passed
88
+ # through verbatim. To render in another locale, see {#with_lang}.
89
+ #
90
+ # A rejected template name is a FAILED RESULT, not an exception: a hostile
91
+ # name is a fact about the request, not a bug in the program. A name that
92
+ # is not a String at all IS a bug in the program, and raises.
93
+ def generate(name, params = {})
94
+ sources = template_root!.resolve(name)
95
+ render(sources, params, origin: :rendered, template: Echo.bounded(name))
96
+ rescue TemplateRoot::Rejected => e
97
+ Result.failed(rejection(e, :generate))
98
+ end
99
+
100
+ # Renders sources the APPLICATION supplies, returning a {Result}.
101
+ #
102
+ # For templates that do not live in a directory this gem can see: fetched
103
+ # from object storage, stored in a database, or written inline. Fetching
104
+ # them stays the application's act — nothing here opens a socket.
105
+ #
106
+ # `assets_dir:` is per call rather than per client, because bundled assets
107
+ # belong to a template rather than to a deployment. Without it, bundled
108
+ # image sources are disabled: inline sources have no directory of their
109
+ # own. The locale is overridden the same way as for {#generate}, with
110
+ # {#with_lang} — one spelling, not two.
111
+ def generate_source(template:, definitions: nil, assets_dir: nil, params: {})
112
+ @settings.lockdown.source_entrance!
113
+ sources = Sources.new(template: template, definitions: definitions,
114
+ assets_dir: assets_dir)
115
+ render(sources, params, origin: :source)
116
+ end
117
+
118
+ # Re-enters an archived document, so bytes signed some time ago can be
119
+ # verified — or re-signed — without hand-building an artifact.
120
+ #
121
+ # The result is marked as LOADED: its bytes are the caller's rather than
122
+ # this client's own render, which is a distinction a strict client acts
123
+ # on. `page_count` is nil, honestly: nothing here laid anything out.
124
+ def artifact(bytes)
125
+ DocumentArtifact.new(bytes: bytes, diagnostics: [], client: self, origin: :loaded)
126
+ end
127
+
128
+ # Signs an artifact with `provider`, returning a {Result}. The signed
129
+ # bytes begin with the input byte for byte — signing appends a revision.
130
+ #
131
+ # `provider` is a {LocalPem} (or another provider object), or the NAME of
132
+ # one registered in configuration. A strict client takes the name only.
133
+ def sign(artifact, provider)
134
+ signer = @settings.lockdown.provider!(provider)
135
+ @settings.lockdown.signable!(artifact)
136
+ @settings.log.timed(:sign) { signed(artifact, signer) }
137
+ end
138
+
139
+ # Verifies an artifact against trust anchors, returning a {Result} whose
140
+ # value is a {VerificationReport}.
141
+ #
142
+ # Anchors are required and are given as paths (`anchors:`, one or several)
143
+ # or as PEM bytes (`anchors_pem:`, which may carry several concatenated).
144
+ # Which form you passed is explicit rather than sniffed, and passing both
145
+ # raises rather than silently preferring one. There is no fallback to the
146
+ # machine's trust store, because the engine never consults one — a default
147
+ # would answer a different question than you asked.
148
+ #
149
+ # A signature that does not verify is a FAILED result that still carries
150
+ # the report, so `not_checked` reaches you either way.
151
+ def verify(artifact, anchors: nil, anchors_pem: nil)
152
+ pem = anchor_material(anchors, anchors_pem)
153
+ @settings.log.timed(:verify) do
154
+ Outcome.verdict(@engine.verify(pdf: artifact.bytes, anchors: pem))
155
+ end
156
+ rescue MaterialUnreadable => e
157
+ Result.failed(Failure.new(step: :verify, kind: e.kind, message: e.message))
158
+ end
159
+
160
+ protected
161
+
162
+ # Written only by {#with_lang}, on a copy that is not yet visible to
163
+ # anyone — a derived client is built, not mutated.
164
+ attr_writer :settings
165
+
166
+ private
167
+
168
+ def render(sources, params, origin:, **fields)
169
+ request = Request.new(sources: sources, params: params, lang: @settings.lang,
170
+ font_dirs: @settings.font_dirs, locale_dirs: @settings.locale_dirs)
171
+ @settings.log.timed(:generate, **fields) do
172
+ snapshot = @engine.render(request.json)
173
+ Outcome.document(snapshot, step: :generate, client: self, origin: origin)
174
+ end
175
+ end
176
+
177
+ # The signed document inherits the origin of what it signed: appending a
178
+ # revision does not launder where the document came from.
179
+ def signed(artifact, provider)
180
+ snapshot = @engine.sign(
181
+ pdf: artifact.bytes, key: provider.key, certificate: provider.certificate,
182
+ passphrase: provider.passphrase
183
+ )
184
+ Outcome.document(snapshot, step: :sign, client: self, origin: artifact.origin)
185
+ rescue MaterialUnreadable => e
186
+ Result.failed(Failure.new(step: :sign, kind: e.kind, message: e.message))
187
+ end
188
+
189
+ def template_root!
190
+ return template_root if template_root
191
+
192
+ raise UsageError,
193
+ "no template root: pass Shojiku::Client.new(templates: …), set it with " \
194
+ "Shojiku.configure, or set SHOJIKU_TEMPLATE_ROOT (which `env: false` " \
195
+ "disables). Sources you already hold go to `generate_source`."
196
+ end
197
+
198
+ def anchor_material(paths, pem)
199
+ raise UsageError, "verify takes either #{ANCHOR_FORMS}, not both" if paths && pem
200
+ return pem if pem
201
+ raise UsageError, "verify needs #{ANCHOR_FORMS}" if paths.nil?
202
+
203
+ Array(paths).map { |path| Material.read(path, "anchor_unreadable") }.join("\n")
204
+ end
205
+
206
+ def rejection(error, step)
207
+ cause = if error.cause_message
208
+ Failure.new(step: step, kind: "io", message: error.cause_message)
209
+ end
210
+ Failure.new(step: step, kind: error.kind, message: error.message, cause: cause)
211
+ end
212
+ end
213
+ end