poli-page 0.9.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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +62 -0
  3. data/CODE_OF_CONDUCT.md +38 -0
  4. data/LICENSE +21 -0
  5. data/MIGRATION.md +68 -0
  6. data/README.md +376 -0
  7. data/SECURITY.md +100 -0
  8. data/lib/poli_page/client.rb +228 -0
  9. data/lib/poli_page/documents.rb +148 -0
  10. data/lib/poli_page/errors.rb +157 -0
  11. data/lib/poli_page/inputs/inline_mode_input.rb +31 -0
  12. data/lib/poli_page/inputs/project_mode_input.rb +38 -0
  13. data/lib/poli_page/inputs/thumbnail_options.rb +25 -0
  14. data/lib/poli_page/internal/constants.rb +38 -0
  15. data/lib/poli_page/internal/http.rb +123 -0
  16. data/lib/poli_page/internal/presigned_fetch.rb +89 -0
  17. data/lib/poli_page/internal/transport.rb +98 -0
  18. data/lib/poli_page/internal/uuid.rb +20 -0
  19. data/lib/poli_page/internal/wire.rb +49 -0
  20. data/lib/poli_page/models/document_descriptor.rb +52 -0
  21. data/lib/poli_page/models/document_preview_result.rb +10 -0
  22. data/lib/poli_page/models/orientation.rb +15 -0
  23. data/lib/poli_page/models/page_format.rb +23 -0
  24. data/lib/poli_page/models/preview_result.rb +9 -0
  25. data/lib/poli_page/models/thumbnail.rb +8 -0
  26. data/lib/poli_page/render.rb +163 -0
  27. data/lib/poli_page/render_to_file.rb +52 -0
  28. data/lib/poli_page/request_event.rb +17 -0
  29. data/lib/poli_page/response_event.rb +15 -0
  30. data/lib/poli_page/retry_event.rb +12 -0
  31. data/lib/poli_page/version.rb +5 -0
  32. data/lib/poli_page.rb +28 -0
  33. data/sig/poli_page/client.rbs +46 -0
  34. data/sig/poli_page/documents.rbs +12 -0
  35. data/sig/poli_page/errors.rbs +84 -0
  36. data/sig/poli_page/models.rbs +106 -0
  37. data/sig/poli_page/render.rbs +32 -0
  38. data/sig/poli_page/request_event.rbs +9 -0
  39. data/sig/poli_page/response_event.rbs +9 -0
  40. data/sig/poli_page/retry_event.rbs +9 -0
  41. data/sig/poli_page.rbs +3 -0
  42. metadata +87 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: da205dffa69777d3da13ab521d2390d2dd1e06474ce6851f99d806524f7efd19
4
+ data.tar.gz: bdbfb7093b0c615e2ba62910e8acd8a5fbf269d303b725ee92d35e01ac1d32d5
5
+ SHA512:
6
+ metadata.gz: 79325da9ac0918bab1e86de4daf459493c1bc5897fc63413905688f01c6c3d130891af2682746a138414591bbf4dcda3e4ef241ba478383f01b13063c02d5075
7
+ data.tar.gz: 8c7c5d9389e5facab3da4552256d3a6b7206687d9811cc321a37f45d434c4c2ae4fc73570b3055554cc43ae49403d691d50059e6b50d8c6606dcef18928e582a
data/CHANGELOG.md ADDED
@@ -0,0 +1,62 @@
1
+ # Changelog
2
+
3
+ All notable changes to `poli-page` (the Poli Page SDK for Ruby) are documented
4
+ here.
5
+
6
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
+ Breaking changes between major versions are summarized in
9
+ [MIGRATION.md](MIGRATION.md).
10
+
11
+ ## [Unreleased]
12
+
13
+ ## [0.9.0] - 2026-06-18
14
+
15
+ ### Added — first release
16
+
17
+ - **`PoliPage::Client`** with `render` and `documents` namespaces, sync
18
+ retry loop, hook firing, and thread-safe accessors.
19
+ - **`render.pdf`**, **`render.pdf_stream`** (block + Enumerator forms),
20
+ **`render.preview`**, **`render.document`**.
21
+ - **`documents.get`**, **`documents.preview`**, **`documents.thumbnails`**,
22
+ **`documents.delete`**.
23
+ - **`Client#render_to_file`** — streams the PDF straight to disk with
24
+ bounded memory.
25
+ - **`DocumentDescriptor#download_pdf`** — fluent helper that fetches the
26
+ PDF bytes from the descriptor's presigned URL.
27
+ - **Class hierarchy errors** rooted at `PoliPage::Error < StandardError`:
28
+ `ValidationError` (400), `AuthenticationError` (401),
29
+ `PermissionDeniedError` (403), `NotFoundError` (404), `GoneError` (410),
30
+ `RateLimitError` (429), `APIError` (other 4xx/5xx),
31
+ `InvalidOptionsError`, `ConnectionError`, `TimeoutError`,
32
+ `DownloadError`, `InternalError`. Predicate helpers
33
+ (`auth_error?`/`rate_limit_error?`/`validation_error?`/
34
+ `network_error?`/`retryable?`) for users who prefer a single
35
+ `rescue PoliPage::Error => e` clause.
36
+ - **`PoliPage::ErrorCodes`** module with the 21 known API code constants.
37
+ - **Auto retry** on 5xx, 429, network errors, and timeouts. Exponential
38
+ backoff with jitter in `[0.5, 1.5)`; honours `Retry-After` (integer
39
+ seconds or HTTP-date), capped at 30 s.
40
+ - **Auto-generated `Idempotency-Key`** (UUID v4) on every POST; override
41
+ via per-call `idempotency_key:` kwarg.
42
+ - **Observability hooks**: `on_retry:` (receives `PoliPage::RetryEvent`)
43
+ and `on_error:` (receives the `PoliPage::Error` directly). Hook
44
+ exceptions never break the request.
45
+ - **Frozen `Data.define` value objects** for inputs and responses:
46
+ `PreviewResult`, `DocumentDescriptor`, `DocumentPreviewResult`,
47
+ `Thumbnail`, `ProjectModeInput`, `InlineModeInput`, `ThumbnailOptions`,
48
+ `RetryEvent`.
49
+ - **`PageFormat::FORMATS`** and **`Orientation::ORIENTATIONS`** — frozen
50
+ `Set`s of valid strings with `.valid?` predicates.
51
+ - **Zero runtime dependencies** — stdlib `Net::HTTP`, `JSON`, `URI`,
52
+ `SecureRandom`, `Logger` only.
53
+ - **RBS signatures** under `sig/` for the public surface; checked via
54
+ `bundle exec steep check` and `bundle exec rbs validate`.
55
+ - **YARD `@example` blocks** on every public method.
56
+ - **CI matrix**: Ruby 3.2 / 3.3 / 3.4 on Ubuntu, plus one job each on
57
+ macOS and Windows with 3.4. RuboCop + Steep + RSpec + bundler-audit
58
+ + gem build + install smoke.
59
+
60
+ ### Requirements
61
+
62
+ Ruby >= 3.2.
@@ -0,0 +1,38 @@
1
+ # Code of Conduct
2
+
3
+ This project adopts the **Contributor Covenant, version 2.1**, in full and
4
+ without modification. The canonical text lives at:
5
+
6
+ <https://www.contributor-covenant.org/version/2/1/code_of_conduct/>
7
+
8
+ By participating in this project — opening issues, submitting pull
9
+ requests, reviewing changes, or interacting in any project venue — you
10
+ agree to abide by it.
11
+
12
+ ## Scope
13
+
14
+ The Code of Conduct applies to all project spaces, including this
15
+ repository's issues, pull requests, and discussions, as well as any
16
+ public space where an individual is representing the project or its
17
+ community.
18
+
19
+ ## Reporting
20
+
21
+ Report any violation to **conduct@poli.page**. Reports are reviewed by
22
+ the project maintainers; reporter identity is kept confidential.
23
+ Vulnerability reports go to a different address — see
24
+ [`SECURITY.md`](SECURITY.md).
25
+
26
+ ## Enforcement
27
+
28
+ Maintainers follow the **Enforcement Guidelines** documented in section 5
29
+ of the canonical Code of Conduct (linked above). Outcomes range from a
30
+ private warning to a permanent ban, scaled to the impact of the
31
+ violation.
32
+
33
+ ## Attribution
34
+
35
+ This Code of Conduct is the
36
+ [Contributor Covenant, v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html),
37
+ authored by Coraline Ada Ehmke and contributors, released under the
38
+ [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Poli Page
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/MIGRATION.md ADDED
@@ -0,0 +1,68 @@
1
+ # Migration Guide
2
+
3
+ This file documents breaking changes between major versions of `poli-page`
4
+ (the Poli Page SDK for Ruby). The SDK follows
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html): breaking changes
6
+ only ship in major version bumps and always come with an entry here.
7
+
8
+ ## 1.0
9
+
10
+ The first stable release. No prior versions of `poli-page` were published to
11
+ RubyGems — treat `1.0.0` as the starting point.
12
+
13
+ ### Surface
14
+
15
+ ```ruby
16
+ require "poli_page"
17
+
18
+ client = PoliPage::Client.new(api_key: ENV.fetch("POLI_PAGE_API_KEY"))
19
+
20
+ # Render namespace
21
+ # render.pdf, render.pdf_stream, render.document → project mode only
22
+ # render.preview → accepts both project mode and inline HTML
23
+ client.render.pdf(project:, template:, version:, data:, ...) # → String of bytes
24
+ client.render.pdf_stream(project:, template:, ...) { |chunk| ... } # → block or Enumerator
25
+ client.render.preview(template:, data:, ...) # → PreviewResult
26
+ client.render.document(project:, template:, ...) # → DocumentDescriptor
27
+
28
+ # Documents namespace
29
+ client.documents.get(id) # → DocumentDescriptor
30
+ client.documents.preview(id) # → DocumentPreviewResult { html, page_count }
31
+ client.documents.thumbnails(id, **opts) # → Array<Thumbnail>
32
+ client.documents.delete(id) # → nil
33
+
34
+ # File helper (instance method on Client)
35
+ client.render_to_file(path, project:, template:, version:, data:, ...) # → nil
36
+ ```
37
+
38
+ ### Method casing
39
+
40
+ All SDK methods are snake_case (`render.pdf_stream`, `documents.thumbnails`).
41
+ Wire JSON uses camelCase; the SDK translates at the transport seam via
42
+ `PoliPage::Internal::Wire.to_wire` / `from_wire`. Callers always pass and
43
+ receive snake_case kwargs / `Data.define` accessors.
44
+
45
+ ### Errors
46
+
47
+ Errors are dispatched by class:
48
+
49
+ ```ruby
50
+ begin
51
+ client.render.pdf(...)
52
+ rescue PoliPage::AuthenticationError, PoliPage::PermissionDeniedError => e
53
+ refresh_credentials(e)
54
+ rescue PoliPage::RateLimitError => e
55
+ queue_for_later(e.request_id)
56
+ rescue PoliPage::Error => e
57
+ raise
58
+ end
59
+ ```
60
+
61
+ Predicate methods (`auth_error?`, `rate_limit_error?`, `validation_error?`,
62
+ `network_error?`, `retryable?`) are available on the base class for callers
63
+ who prefer `rescue PoliPage::Error => e` with branching.
64
+
65
+ ### MSRV
66
+
67
+ `required_ruby_version = ">= 3.2"`. Bumps to the MSRV are MINOR-version
68
+ releases with a note in this file.
data/README.md ADDED
@@ -0,0 +1,376 @@
1
+ # Poli Page SDK for Ruby
2
+
3
+ [![Gem](https://img.shields.io/gem/v/poli-page?style=flat&logo=ruby&logoColor=ffffff&label=Gem)](https://rubygems.org/gems/poli-page)
4
+ [![Downloads](https://img.shields.io/gem/dt/poli-page?style=flat&logo=ruby&logoColor=ffffff&label=Downloads)](https://rubygems.org/gems/poli-page)
5
+ [![Ci](https://img.shields.io/github/actions/workflow/status/poli-page/sdk-ruby/ci.yml?branch=main&style=flat&logo=githubactions&logoColor=ffffff&label=Ci)](https://github.com/poli-page/sdk-ruby/actions/workflows/ci.yml)
6
+ [![Codeql](https://img.shields.io/github/actions/workflow/status/poli-page/sdk-ruby/codeql.yml?branch=main&style=flat&logo=github&logoColor=ffffff&label=Codeql)](https://github.com/poli-page/sdk-ruby/actions/workflows/codeql.yml)
7
+ [![Coverage](https://img.shields.io/codecov/c/github/poli-page/sdk-ruby?style=flat&logo=codecov&logoColor=ffffff&label=Coverage)](https://codecov.io/gh/poli-page/sdk-ruby)
8
+ [![Ruby](https://img.shields.io/badge/Ruby-3.2%20%7C%203.3%20%7C%203.4-blue?style=flat&logo=ruby&logoColor=ffffff)](https://github.com/poli-page/sdk-ruby/blob/main/.github/workflows/ci.yml)
9
+ [![Types](https://img.shields.io/badge/Types-RBS-blue?style=flat&logo=ruby&logoColor=ffffff)](https://github.com/poli-page/sdk-ruby/tree/main/sig)
10
+ [![Style](https://img.shields.io/badge/Style-Rubocop-blue?style=flat&logo=ruby&logoColor=ffffff)](https://github.com/rubocop/rubocop)
11
+ [![Deps](https://img.shields.io/badge/Deps-up%20to%20date-brightgreen?style=flat&logo=ruby&logoColor=ffffff)](https://github.com/poli-page/sdk-ruby/network/dependencies)
12
+ [![Docs](https://img.shields.io/badge/Docs-online-brightgreen?style=flat&logo=readthedocs&logoColor=ffffff)](https://poli-page.github.io/sdk-ruby/)
13
+ [![License](https://img.shields.io/github/license/poli-page/sdk-ruby?style=flat&logo=gnu&logoColor=ffffff&label=License)](LICENSE)
14
+
15
+ Official Ruby SDK for [Poli Page](https://poli.page) — render polished PDFs
16
+ from HTML templates via the Poli Page API.
17
+
18
+ → **Documentation**: <https://poli-page.github.io/sdk-ruby/>
19
+
20
+ ## Install
21
+
22
+ ```ruby
23
+ # Gemfile
24
+ gem "poli-page"
25
+ ```
26
+
27
+ ```sh
28
+ bundle install
29
+ # or
30
+ gem install poli-page
31
+ ```
32
+
33
+ Requires Ruby **>= 3.2**.
34
+
35
+ The gem has **zero runtime dependencies** — only the stdlib (`net/http`,
36
+ `json`, `uri`, `securerandom`, `logger`, `openssl`).
37
+
38
+ ## Quick start
39
+
40
+ ### Project mode — render a published template by slug
41
+
42
+ ```ruby
43
+ require "poli_page"
44
+
45
+ client = PoliPage::Client.new(api_key: ENV.fetch("POLI_PAGE_API_KEY"))
46
+
47
+ pdf = client.render.pdf(
48
+ project: "getting-started",
49
+ template: "welcome",
50
+ version: "1.0.0",
51
+ data: { name: "World" }
52
+ )
53
+ # pdf is a binary-encoded String
54
+ ```
55
+
56
+ Every Poli Page org comes pre-provisioned with a `getting-started/welcome`
57
+ template, so the snippet above runs as-is the moment you have an API key —
58
+ no project setup needed. For your own templates, swap the slugs once
59
+ you've pushed a version with the `poli` CLI:
60
+
61
+ ```ruby
62
+ pdf = client.render.pdf(
63
+ project: "billing",
64
+ template: "invoice",
65
+ version: "1.0.0",
66
+ data: { invoice_number: "INV-001", total: 1280 }
67
+ )
68
+ ```
69
+
70
+ ### Preview inline HTML
71
+
72
+ `render.preview` accepts raw HTML for live editing and visual inspection
73
+ without producing a stored document. Use this for editor previews or
74
+ layout tests.
75
+
76
+ ```ruby
77
+ result = client.render.preview(
78
+ template: "<h1>Hello {{ name }}</h1>",
79
+ data: { name: "World" }
80
+ )
81
+ puts "Rendered #{result.total_pages} page(s) in #{result.environment} mode"
82
+ ```
83
+
84
+ `render.pdf`, `render.pdf_stream`, and `render.document` **require project
85
+ mode** — `project` + `template`, optionally pinned to a specific
86
+ `version` (omit to render the current draft). Inline HTML is only
87
+ accepted by `render.preview`.
88
+
89
+ ### Write a PDF to disk
90
+
91
+ ```ruby
92
+ client.render_to_file(
93
+ "./welcome.pdf",
94
+ project: "getting-started", template: "welcome", version: "1.0.0",
95
+ data: { name: "World" }
96
+ )
97
+ ```
98
+
99
+ `render_to_file` streams the response bytes directly to disk via
100
+ `render.pdf_stream` — bounded memory regardless of PDF size. Parent
101
+ directories are created on the fly.
102
+
103
+ ### Try it locally — runnable demo
104
+
105
+ ```sh
106
+ ruby examples/demo.rb
107
+ ```
108
+
109
+ The demo walks every public method end-to-end and writes the results to
110
+ `examples/output/`. First run prompts for a `pp_test_*` key and saves it
111
+ to `.env` at the project root. Subsequent runs are silent. The `POLI_PAGE_API_KEY`
112
+ env var wins over the file when set.
113
+
114
+ ### Stream — for large PDFs or piping to S3 / HTTP responses
115
+
116
+ ```ruby
117
+ File.open("invoice.pdf", "wb") do |io|
118
+ client.render.pdf_stream(
119
+ project: "billing", template: "invoice", version: "1.0.0",
120
+ data: { invoice_number: "INV-001" }
121
+ ) do |chunk|
122
+ io.write(chunk)
123
+ end
124
+ end
125
+ ```
126
+
127
+ Without a block, `pdf_stream` returns an `Enumerator` that composes with
128
+ `Enumerable`:
129
+
130
+ ```ruby
131
+ enum = client.render.pdf_stream(project: "billing", template: "invoice", version: "1.0.0", data: data)
132
+ enum.each { |chunk| s3.upload_part(part_number: n += 1, body: chunk) }
133
+ ```
134
+
135
+ ## Working with stored documents
136
+
137
+ Every render produces a stored document, accessible via `document_id` for
138
+ later download or thumbnails. `render.pdf` and `render.pdf_stream` are
139
+ conveniences that chain a presigned-URL fetch internally to return bytes;
140
+ `render.document` returns just the descriptor (skip the auto-download
141
+ when you'll fetch the bytes later).
142
+
143
+ ```ruby
144
+ # 1. Render and store
145
+ doc = client.render.document(
146
+ project: "billing",
147
+ template: "invoice",
148
+ version: "1.0.0",
149
+ data: { invoice_number: "INV-001" },
150
+ metadata: { customer_id: "cust_123" } # your own audit data
151
+ )
152
+ # doc.document_id, doc.page_count, doc.size_bytes, doc.presigned_pdf_url, doc.metadata, ...
153
+
154
+ # 2. Persist doc.document_id in your database
155
+ db.invoices.update(id: "INV-001", document_id: doc.document_id)
156
+
157
+ # 3. Later, fetch a fresh presigned URL + download
158
+ fresh = client.documents.get(doc.document_id)
159
+ pdf = fresh.download_pdf
160
+
161
+ # 4. Generate thumbnails (Starter+ tier)
162
+ thumbs = client.documents.thumbnails(doc.document_id, width: 320, format: "png")
163
+
164
+ # 5. When done, soft-delete
165
+ client.documents.delete(doc.document_id)
166
+ ```
167
+
168
+ The presigned URL has a 15-minute TTL. If `download_pdf` raises
169
+ `PoliPage::DownloadError` with `status: 403` from S3, call
170
+ `documents.get(id)` to refresh and retry.
171
+
172
+ ## Authentication & environments
173
+
174
+ The mode is determined by the API key prefix:
175
+
176
+ - `pp_test_…` → sandbox mode (not billed, generous rate limits)
177
+ - `pp_live_…` → live mode (billed, production rate limits)
178
+ - `pp_sa_…` → service-account keys; environment matches the SA's
179
+ configuration (sandbox or live)
180
+
181
+ All prefixes hit the same endpoint (`https://api.poli.page`). The SDK
182
+ passes the key through as a `Bearer` token and never inspects the prefix —
183
+ pick whichever fits your deploy model.
184
+
185
+ ## Methods
186
+
187
+ | Method | Returns | Description |
188
+ | ----------------------------------------------- | ---------------------------------- | --------------------------------------------------- |
189
+ | `client.render.pdf(**input)` | `String` (binary bytes) | Render a PDF, return bytes |
190
+ | `client.render.pdf_stream(**input) { \|c\| … }` | `Enumerator` or yields chunks | Render and stream the response |
191
+ | `client.render.preview(**input)` | `PoliPage::PreviewResult` | Paginated HTML preview |
192
+ | `client.render.document(**input)` | `PoliPage::DocumentDescriptor` | Render and return descriptor (skip auto-download) |
193
+ | `client.documents.get(id)` | `PoliPage::DocumentDescriptor` | Retrieve a stored document |
194
+ | `client.documents.preview(id)` | `PoliPage::DocumentPreviewResult` | Stored document's paginated HTML |
195
+ | `client.documents.thumbnails(id, **opts)` | `Array<PoliPage::Thumbnail>` | Page thumbnails (PNG/JPEG, base64) |
196
+ | `client.documents.delete(id)` | `nil` | Soft-delete a stored document |
197
+ | `client.render_to_file(path, **input)` | `nil` | Render and stream to disk |
198
+
199
+ ## Configuration
200
+
201
+ | Option | Type | Default | Description |
202
+ | ------------- | -------------------------- | -------------------------- | -------------------------------------------- |
203
+ | `api_key:` | `String` | (required) | `pp_test_*` or `pp_live_*` API key |
204
+ | `base_url:` | `String` | `"https://api.poli.page"` | API base URL |
205
+ | `max_retries:`| `Integer` | `2` | Max retry attempts on retryable errors |
206
+ | `retry_delay:`| `Numeric` (seconds) | `0.5` | Base delay before the first retry |
207
+ | `timeout:` | `Numeric` (seconds) | `60` | Per-request timeout |
208
+ | `logger:` | `Logger` (any duck type) | `nil` | Hook errors and DEBUG events here |
209
+ | `on_retry:` | `#call(PoliPage::RetryEvent)`| `nil` | Called when a retry is scheduled |
210
+ | `on_error:` | `#call(PoliPage::Error)` | `nil` | Called when a call terminates in error |
211
+ | `proxy:` | `String` (URL) | `nil` (uses env) | Override HTTP proxy; e.g. `"http://u:p@host:8080"` |
212
+ | `ca_file:` | `String` (path) | `nil` | Custom CA bundle (corp MITM, private PKI) |
213
+ | `ca_path:` | `String` (path) | `nil` | Custom CA directory (hashed cert dir) |
214
+
215
+ `http_proxy`, `https_proxy`, and `no_proxy` environment variables are
216
+ honored automatically by `Net::HTTP`'s `:ENV` proxy resolution — no
217
+ configuration needed in the common case. Pass `proxy:` to override.
218
+ For TLS verification against a private CA (e.g. behind a corporate
219
+ MITM-terminating proxy), point `ca_file:` at a PEM bundle.
220
+
221
+ ## Error handling
222
+
223
+ Errors are dispatched by class — the rescue-friendly path:
224
+
225
+ ```ruby
226
+ require "poli_page"
227
+
228
+ begin
229
+ client.render.pdf(project: "billing", template: "invoice", version: "1.0.0", data: data)
230
+ rescue PoliPage::AuthenticationError, PoliPage::PermissionDeniedError => e
231
+ refresh_credentials(e)
232
+ rescue PoliPage::RateLimitError => e
233
+ queue_for_later(e.request_id)
234
+ rescue PoliPage::ValidationError => e
235
+ logger.error("Bad input: #{e.message}")
236
+ rescue PoliPage::GoneError => e
237
+ mark_document_gone(e.code)
238
+ rescue PoliPage::Error => e
239
+ raise
240
+ end
241
+ ```
242
+
243
+ Predicate helpers on the base class are kept for callers who prefer a
244
+ single `rescue PoliPage::Error => e` clause:
245
+
246
+ ```ruby
247
+ rescue PoliPage::Error => e
248
+ return refresh_credentials if e.auth_error?
249
+ return queue_for_later if e.rate_limit_error?
250
+ logger.error(e.code, e.status, e.request_id)
251
+ raise unless e.retryable? # SDK already retried up to max_retries
252
+ end
253
+ ```
254
+
255
+ For lifecycle and billing failures, route the user to actionable messages
256
+ rather than treating them as opaque:
257
+
258
+ ```ruby
259
+ rescue PoliPage::Error => e
260
+ case e.code
261
+ when "PAYMENT_REQUIRED" then show_banner("Subscription has unpaid invoices.")
262
+ when "ORGANIZATION_CANCELLED" then show_banner("Subscription cancelled — service is read-only.")
263
+ when "ORGANIZATION_PURGED" then show_banner("Organization has been purged.")
264
+ when "DOCUMENT_NOT_FOUND" then render_404
265
+ when "GONE" then render_410 # document was soft-deleted
266
+ else
267
+ raise
268
+ end
269
+ end
270
+ ```
271
+
272
+ The full list of known API codes lives in `PoliPage::ErrorCodes` (see
273
+ `lib/poli_page/errors.rb`).
274
+
275
+ → Full error reference: <https://poli-page.github.io/sdk-ruby/reference/errors/>
276
+
277
+ ## Cancellation
278
+
279
+ Ruby has no async cancellation primitive comparable to `AbortSignal`.
280
+ Three mechanisms are available:
281
+
282
+ - **Per-request timeout** via the constructor's `timeout:` option — the
283
+ most common case.
284
+ - **`Timeout.timeout` wrapping** for caller-side deadlines:
285
+ ```ruby
286
+ begin
287
+ pdf = Timeout.timeout(10) { client.render.pdf(...) }
288
+ rescue Timeout::Error
289
+ # ... handle the deadline; the SDK does NOT translate this into PoliPage::TimeoutError
290
+ end
291
+ ```
292
+ - **`Thread#raise`** from a sibling thread for advanced cases; the SDK's
293
+ blocking `sleep` between retries honours the interrupt.
294
+
295
+ For streaming methods, the block is the cancellation point: `break` or
296
+ `raise` out of the block to stop reading mid-stream.
297
+
298
+ ## Observability
299
+
300
+ Two hooks fire at well-defined points. They're synchronous, optional, and
301
+ never break the request:
302
+
303
+ ```ruby
304
+ client = PoliPage::Client.new(
305
+ api_key: ENV.fetch("POLI_PAGE_API_KEY"),
306
+ logger: Logger.new($stdout),
307
+ on_retry: ->(event) { Statsd.increment("polipage.retry", tags: ["code:#{event.reason.code}"]) },
308
+ on_error: ->(err) { Sentry.capture_exception(err) }
309
+ )
310
+ ```
311
+
312
+ Hook exceptions are swallowed — a broken metrics path never crashes a
313
+ render. The injected `logger:` works with any `Logger`-compatible object
314
+ (`lograge`, `ougai`, `semantic_logger`, etc.).
315
+
316
+ ## Retries & idempotency
317
+
318
+ The SDK retries on **5xx**, **429**, **network errors**, and **timeouts**.
319
+ Backoff is exponential (`retry_delay * 2**(attempt - 1)`) with jitter in
320
+ `[0.5, 1.5)`, capped by `Retry-After` when the server provides it
321
+ (integer seconds or HTTP-date; capped at 30 s).
322
+
323
+ Every POST sends an auto-generated `Idempotency-Key` (UUID v4); pass
324
+ `idempotency_key:` to override.
325
+
326
+ ```ruby
327
+ client.render.pdf(project: "billing", template: "invoice", version: "1.0.0",
328
+ data: data, idempotency_key: "render-INV-001-2026-05")
329
+ ```
330
+
331
+ ## Type system
332
+
333
+ The gem ships RBS signatures under `sig/`. Steep checks them against the
334
+ implementation in CI. Consumers can opt into project-mode validation via
335
+ required kwargs at runtime — `Client#render.pdf` raises `ArgumentError`
336
+ immediately if `project:`, `template:`, or `data:` are omitted.
337
+
338
+ For Sorbet users: `.rbi` shipping is a post-1.0 enhancement.
339
+
340
+ ## Concurrency & thread-safety
341
+
342
+ A single `PoliPage::Client` instance is safe to share across threads.
343
+ Configuration is immutable after `#initialize`; each request opens its
344
+ own `Net::HTTP` connection.
345
+
346
+ ```ruby
347
+ CLIENT = PoliPage::Client.new(api_key: ENV.fetch("POLI_PAGE_API_KEY")).freeze
348
+
349
+ pdfs = ids.map do |id|
350
+ Thread.new { CLIENT.render.pdf(project: "billing", template: "invoice", version: "1.0.0", data: { invoice_number: id }) }
351
+ end.map(&:value)
352
+ ```
353
+
354
+ Build the client once at boot. Don't construct a new client per request.
355
+
356
+ ## Runtime support
357
+
358
+ - Ruby 3.1+ (CRuby/MRI). Latest two minor releases tested in CI.
359
+ - JRuby 9.4+ on a best-effort basis.
360
+ - Linux and macOS in CI. Windows is supported but not exercised in CI.
361
+
362
+ ## Requirements
363
+
364
+ Ruby **>= 3.2**. Tested in CI against 3.2, 3.3, and 3.4 on Ubuntu, plus
365
+ one job each on macOS and Windows with 3.4.
366
+
367
+ ## Documentation & support
368
+
369
+ - Platform docs: [docs.poli.page](https://docs.poli.page)
370
+ - SDK documentation: [poli-page.github.io/sdk-ruby](https://poli-page.github.io/sdk-ruby/)
371
+ - Sign up & generate API keys: [app.poli.page](https://app.poli.page)
372
+ - Issues: [github.com/poli-page/sdk-ruby/issues](https://github.com/poli-page/sdk-ruby/issues)
373
+
374
+ ## License
375
+
376
+ [MIT](LICENSE) © Poli Page
data/SECURITY.md ADDED
@@ -0,0 +1,100 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ Report suspected vulnerabilities **privately** to **security@poli.page**.
6
+
7
+ Please do **not** file a public GitHub issue, open a pull request that
8
+ demonstrates the flaw on `main`, or discuss the issue in public channels
9
+ until a fix has shipped.
10
+
11
+ Useful information to include:
12
+
13
+ - A short description of the issue and its impact.
14
+ - Affected versions (run `bundle info poli-page` or check `Gemfile.lock`).
15
+ - Reproduction steps or a proof-of-concept (minimal Ruby script
16
+ preferred).
17
+ - Suggested remediation, if you have one.
18
+
19
+ GitHub's **private vulnerability reporting** is also enabled on this
20
+ repository — see
21
+ [Report a vulnerability](https://github.com/poli-page/sdk-ruby/security/advisories/new).
22
+ Either channel reaches the same maintainers.
23
+
24
+ ## Response Timeline
25
+
26
+ | Stage | Target |
27
+ | --------------------- | --------------------------------------------- |
28
+ | Acknowledgement | within **2 business days** of the report |
29
+ | Triage decision | within **5 business days** |
30
+ | Fix released | typically within **30 days** of triage |
31
+ | Public advisory | published alongside the fix release |
32
+ | Coordinated disclosure| up to **90 days** by default; negotiable |
33
+
34
+ We will keep the reporter informed at each stage and credit them in the
35
+ published advisory unless they prefer to remain anonymous.
36
+
37
+ ## Supported Versions
38
+
39
+ Only the **latest minor version** of `poli-page` receives security
40
+ updates. Older minors do not receive backports — please upgrade.
41
+
42
+ | Version | Supported |
43
+ | ------------- | ------------------ |
44
+ | `1.x` (latest)| Yes |
45
+ | `< 1.x` | No |
46
+
47
+ ## Scope
48
+
49
+ **In scope** — issues in code shipped by this gem:
50
+
51
+ - Credential leakage through logs, exceptions, or `#inspect` output.
52
+ - Request-forgery via SDK-constructed URLs, headers, or bodies.
53
+ - TLS / certificate validation defects in the transport layer.
54
+ - Retry / idempotency invariants that could cause unintended side effects.
55
+ - Denial-of-service via malformed API responses or unbounded resource use.
56
+ - Supply-chain integrity issues in the published `.gem` artifact.
57
+
58
+ **Out of scope:**
59
+
60
+ - Vulnerabilities in the deployed Poli Page API (`api.poli.page`) —
61
+ report those at <https://poli.page/security>.
62
+ - Issues that require running the SDK against an untrusted Poli Page
63
+ base URL (`base_url:` is a trust boundary; pointing the client at a
64
+ malicious host is the caller's call).
65
+ - Issues in development-only dependencies (Gemfile group `:development,
66
+ :test`) that do not ship in the published `.gem`.
67
+ - Findings from automated scanners without a demonstrated impact.
68
+
69
+ ## Hardening This Project Applies
70
+
71
+ - **Zero runtime dependencies** — the SDK uses only the Ruby stdlib for
72
+ HTTP, JSON, randomness, and TLS. The runtime attack surface is what
73
+ ships with Ruby itself.
74
+ - **Lockfile committed and frozen in CI** — `Gemfile.lock` is checked
75
+ in, and CI installs run with `bundle config set --local frozen true`.
76
+ - **Advisory scan in CI** — `bundle-audit check --update` runs on every
77
+ push and PR.
78
+ - **CodeQL "security-and-quality" suite** — runs on push, PR, and
79
+ weekly schedule.
80
+ - **GitHub Actions pinned to commit SHAs** — every third-party Action
81
+ is pinned to a 40-character SHA, not a floating tag.
82
+ - **Dependabot** — weekly updates for `bundler` and `github-actions`.
83
+ - **MFA-required publishing** — the gemspec declares
84
+ `rubygems_mfa_required: "true"`; the per-gem RubyGems API key is
85
+ scoped to `poli-page` only.
86
+ - **Restricted workflow permissions** — every workflow declares the
87
+ minimum `permissions:` block (most are `contents: read`).
88
+
89
+ ## Verifying a Published Release
90
+
91
+ ```sh
92
+ gem fetch poli-page -v <version>
93
+ gem unpack poli-page-<version>.gem
94
+ gem spec poli-page-<version>.gem
95
+ ```
96
+
97
+ The unpacked `.gem` contains the full source — diff it against the
98
+ matching git tag on
99
+ [`github.com/poli-page/sdk-ruby`](https://github.com/poli-page/sdk-ruby)
100
+ to verify the published artifact matches the public history.