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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +62 -0
- data/CODE_OF_CONDUCT.md +38 -0
- data/LICENSE +21 -0
- data/MIGRATION.md +68 -0
- data/README.md +376 -0
- data/SECURITY.md +100 -0
- data/lib/poli_page/client.rb +228 -0
- data/lib/poli_page/documents.rb +148 -0
- data/lib/poli_page/errors.rb +157 -0
- data/lib/poli_page/inputs/inline_mode_input.rb +31 -0
- data/lib/poli_page/inputs/project_mode_input.rb +38 -0
- data/lib/poli_page/inputs/thumbnail_options.rb +25 -0
- data/lib/poli_page/internal/constants.rb +38 -0
- data/lib/poli_page/internal/http.rb +123 -0
- data/lib/poli_page/internal/presigned_fetch.rb +89 -0
- data/lib/poli_page/internal/transport.rb +98 -0
- data/lib/poli_page/internal/uuid.rb +20 -0
- data/lib/poli_page/internal/wire.rb +49 -0
- data/lib/poli_page/models/document_descriptor.rb +52 -0
- data/lib/poli_page/models/document_preview_result.rb +10 -0
- data/lib/poli_page/models/orientation.rb +15 -0
- data/lib/poli_page/models/page_format.rb +23 -0
- data/lib/poli_page/models/preview_result.rb +9 -0
- data/lib/poli_page/models/thumbnail.rb +8 -0
- data/lib/poli_page/render.rb +163 -0
- data/lib/poli_page/render_to_file.rb +52 -0
- data/lib/poli_page/request_event.rb +17 -0
- data/lib/poli_page/response_event.rb +15 -0
- data/lib/poli_page/retry_event.rb +12 -0
- data/lib/poli_page/version.rb +5 -0
- data/lib/poli_page.rb +28 -0
- data/sig/poli_page/client.rbs +46 -0
- data/sig/poli_page/documents.rbs +12 -0
- data/sig/poli_page/errors.rbs +84 -0
- data/sig/poli_page/models.rbs +106 -0
- data/sig/poli_page/render.rbs +32 -0
- data/sig/poli_page/request_event.rbs +9 -0
- data/sig/poli_page/response_event.rbs +9 -0
- data/sig/poli_page/retry_event.rbs +9 -0
- data/sig/poli_page.rbs +3 -0
- 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.
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -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
|
+
[](https://rubygems.org/gems/poli-page)
|
|
4
|
+
[](https://rubygems.org/gems/poli-page)
|
|
5
|
+
[](https://github.com/poli-page/sdk-ruby/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/poli-page/sdk-ruby/actions/workflows/codeql.yml)
|
|
7
|
+
[](https://codecov.io/gh/poli-page/sdk-ruby)
|
|
8
|
+
[](https://github.com/poli-page/sdk-ruby/blob/main/.github/workflows/ci.yml)
|
|
9
|
+
[](https://github.com/poli-page/sdk-ruby/tree/main/sig)
|
|
10
|
+
[](https://github.com/rubocop/rubocop)
|
|
11
|
+
[](https://github.com/poli-page/sdk-ruby/network/dependencies)
|
|
12
|
+
[](https://poli-page.github.io/sdk-ruby/)
|
|
13
|
+
[](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.
|