@pdfcraft-dev/pdf 1.0.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.
- package/LICENSE +21 -0
- package/README.md +207 -0
- package/dist/cjs/client.js +107 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/contract/errors.js +91 -0
- package/dist/cjs/contract/errors.js.map +1 -0
- package/dist/cjs/contract/index.js +23 -0
- package/dist/cjs/contract/index.js.map +1 -0
- package/dist/cjs/contract/render-options.js +111 -0
- package/dist/cjs/contract/render-options.js.map +1 -0
- package/dist/cjs/contract/request.js +43 -0
- package/dist/cjs/contract/request.js.map +1 -0
- package/dist/cjs/errors.js +31 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/index.js +9 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/client.d.ts +36 -0
- package/dist/esm/client.js +103 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/contract/errors.d.ts +75 -0
- package/dist/esm/contract/errors.js +87 -0
- package/dist/esm/contract/errors.js.map +1 -0
- package/dist/esm/contract/index.d.ts +3 -0
- package/dist/esm/contract/index.js +7 -0
- package/dist/esm/contract/index.js.map +1 -0
- package/dist/esm/contract/render-options.d.ts +191 -0
- package/dist/esm/contract/render-options.js +108 -0
- package/dist/esm/contract/render-options.js.map +1 -0
- package/dist/esm/contract/request.d.ts +121 -0
- package/dist/esm/contract/request.js +40 -0
- package/dist/esm/contract/request.js.map +1 -0
- package/dist/esm/errors.d.ts +10 -0
- package/dist/esm/errors.js +26 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gaurav Singh
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# @pdfcraft-dev/pdf
|
|
2
|
+
|
|
3
|
+
**HTML to PDF in one call.** The official TypeScript client for [PDFCraft](https://pdfcraft.dev) —
|
|
4
|
+
POST some HTML or a URL, get back a PDF rendered by real Chromium.
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/@pdfcraft-dev/pdf)
|
|
7
|
+
[](#what-you-are-installing)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @pdfcraft-dev/pdf
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { Renderer } from '@pdfcraft-dev/pdf';
|
|
16
|
+
|
|
17
|
+
const pdfcraft = new Renderer(process.env.PDFCRAFT_API_KEY!);
|
|
18
|
+
|
|
19
|
+
const pdf = await pdfcraft.render({ html: '<h1>Invoice 1042</h1>' });
|
|
20
|
+
await writeFile('invoice.pdf', pdf);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
That is the whole product. No browser to install, no Chromium in your Docker image, no
|
|
24
|
+
`--disable-dev-shm-usage` to discover the hard way. Get a key at
|
|
25
|
+
[pdfcraft.dev](https://pdfcraft.dev) — the free tier is 100 renders a month and needs no
|
|
26
|
+
card.
|
|
27
|
+
|
|
28
|
+
## What you are installing
|
|
29
|
+
|
|
30
|
+
**Zero runtime dependencies.** 24 kB packed — about half of which is source maps, so stack
|
|
31
|
+
traces from inside the client point at real TypeScript — and nothing beneath it in your
|
|
32
|
+
lockfile. The client is `fetch` plus a retry loop; the types are generated from the same
|
|
33
|
+
source file the API validates requests against, so they cannot drift from the server.
|
|
34
|
+
|
|
35
|
+
Ships ESM and CommonJS. Works on Node 18+, Bun, Deno, Cloudflare Workers, Vercel Edge —
|
|
36
|
+
anywhere there is a global `fetch`.
|
|
37
|
+
|
|
38
|
+
## The four methods
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const pdf = await pdfcraft.render({ html }); // Uint8Array (a Buffer in Node)
|
|
42
|
+
const res = await pdfcraft.renderToUrl({ html }); // { url, expires_at, pages, bytes }
|
|
43
|
+
const job = await pdfcraft.renderAsync({ html, callback_url });
|
|
44
|
+
const sta = await pdfcraft.getRender(job.id);
|
|
45
|
+
const use = await pdfcraft.usage(); // { used, limit, resets_at }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`render` streams the bytes back on the same connection — one round trip, typically under
|
|
49
|
+
a second. `renderToUrl` uploads to storage and hands you a signed link instead, which is
|
|
50
|
+
what you want when the PDF is large or the caller is a browser. Both take the same input;
|
|
51
|
+
the method you call decides the output, so there is no `output` field to get wrong.
|
|
52
|
+
|
|
53
|
+
## Page options
|
|
54
|
+
|
|
55
|
+
Every option the API accepts, fully typed, with autocomplete on the enums.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const pdf = await pdfcraft.render({
|
|
59
|
+
html: invoiceHtml,
|
|
60
|
+
options: {
|
|
61
|
+
format: 'A4', // A4 A3 A5 Letter Legal Tabloid
|
|
62
|
+
landscape: false,
|
|
63
|
+
margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' },
|
|
64
|
+
scale: 1.0, // 0.1 – 2.0
|
|
65
|
+
printBackground: true, // on by default here, unlike a browser
|
|
66
|
+
pageRanges: '1-5',
|
|
67
|
+
headerHtml: '<div style="font-size:9px;width:100%;text-align:center">Acme Ltd</div>',
|
|
68
|
+
footerHtml: '<div style="font-size:9px"><span class="pageNumber"></span></div>',
|
|
69
|
+
waitFor: { selector: '#ready', networkIdle: true, delayMs: 0 },
|
|
70
|
+
emulateMedia: 'print', // print | screen
|
|
71
|
+
timeoutMs: 30_000, // ceiling 120_000
|
|
72
|
+
},
|
|
73
|
+
filename: 'invoice-1042.pdf',
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`headerHtml` and `footerHtml` support Chromium's print classes — `pageNumber`, `totalPages`,
|
|
78
|
+
`date`, `title`, `url`. A footer needs a bottom margin big enough to sit in, or it will not
|
|
79
|
+
appear at all.
|
|
80
|
+
|
|
81
|
+
### Rendering a page behind a login
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const { url } = await pdfcraft.renderToUrl({
|
|
85
|
+
url: 'https://app.example.com/reports/42',
|
|
86
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
87
|
+
cookies: [{ name: 'session', value: sessionId }],
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Waiting for the page to be ready
|
|
92
|
+
|
|
93
|
+
A chart that renders from JavaScript is not finished when the DOM loads. Use whichever
|
|
94
|
+
signal your page actually gives you:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
options: { waitFor: { selector: '#chart-rendered' } } // best: explicit
|
|
98
|
+
options: { waitFor: { networkIdle: true } } // good: no requests for 500ms
|
|
99
|
+
options: { waitFor: { delayMs: 1500 } } // last resort: a guess
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Errors
|
|
103
|
+
|
|
104
|
+
Every failure is a `PDFCraftError` with a stable `.code` you can branch on. The codes are
|
|
105
|
+
part of the contract and will not be renamed inside a major version.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { PDFCraftError } from '@pdfcraft-dev/pdf';
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await pdfcraft.render({ html });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error instanceof PDFCraftError) {
|
|
114
|
+
if (error.code === 'quota_exceeded') return showUpgradePrompt();
|
|
115
|
+
if (error.retryable) return queueForLater();
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
| `.code` | HTTP | Means | Billed? |
|
|
122
|
+
| ------------------ | ---- | ---------------------------------------- | --------------------------------------- |
|
|
123
|
+
| `invalid_request` | 400 | Bad option, or both/neither html and url | no |
|
|
124
|
+
| `invalid_api_key` | 401 | Missing, malformed or revoked key | no |
|
|
125
|
+
| `payment_required` | 402 | Subscription past due | no |
|
|
126
|
+
| `not_found` | 404 | Unknown render id | no |
|
|
127
|
+
| `render_timeout` | 408 | The page exceeded `timeoutMs` | no |
|
|
128
|
+
| `render_failed` | 422 | Navigation failed, selector never showed | **yes** — Chromium ran, your HTML broke |
|
|
129
|
+
| `rate_limited` | 429 | Too many requests per second | no |
|
|
130
|
+
| `quota_exceeded` | 429 | Monthly plan limit reached | no |
|
|
131
|
+
| `internal_error` | 500 | Our fault | no |
|
|
132
|
+
|
|
133
|
+
`error.retryable` is true for `network_error`, `429` and `5xx`. Retries on those happen
|
|
134
|
+
automatically — up to three retries with exponential backoff, honouring `Retry-After`. Other
|
|
135
|
+
`4xx` are never retried, because they fail identically however often you ask.
|
|
136
|
+
|
|
137
|
+
## Idempotency
|
|
138
|
+
|
|
139
|
+
Pass a key as the second argument. The same key inside 24 hours returns the original
|
|
140
|
+
render instead of billing you twice — which matters when the thing calling you is a
|
|
141
|
+
webhook handler or a job queue that retries.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
await pdfcraft.render({ html }, `invoice-${invoiceId}`);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Async renders
|
|
148
|
+
|
|
149
|
+
For documents that take a while, or when you do not want to hold a connection open.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const { id } = await pdfcraft.renderAsync({
|
|
153
|
+
html,
|
|
154
|
+
callback_url: 'https://example.com/hooks/pdf',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// later, if you would rather poll than receive
|
|
158
|
+
const { status, url } = await pdfcraft.getRender(id);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The callback is a POST with an `x-signature` header: HMAC-SHA256 of the **raw** request
|
|
162
|
+
body, hex-encoded, keyed with your webhook secret from the dashboard. Verify it before
|
|
163
|
+
trusting the payload.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
167
|
+
|
|
168
|
+
const expected = createHmac('sha256', process.env.PDFCRAFT_WEBHOOK_SECRET!)
|
|
169
|
+
.update(rawBody) // the raw bytes, not the parsed object
|
|
170
|
+
.digest('hex');
|
|
171
|
+
|
|
172
|
+
const given = Buffer.from(req.headers['x-signature'] as string);
|
|
173
|
+
const ok = given.length === expected.length && timingSafeEqual(Buffer.from(expected), given);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Client options
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
new Renderer(apiKey, {
|
|
180
|
+
baseUrl: 'https://api.pdfcraft.dev', // point at a test double in your test suite
|
|
181
|
+
maxRetries: 3,
|
|
182
|
+
timeoutMs: 130_000, // just past the API's own 120s ceiling
|
|
183
|
+
fetch: myInstrumentedFetch, // inject your own for logging or mocking
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Testing against a fake is the reason `fetch` is injectable — you should not need network
|
|
188
|
+
access to run your unit tests.
|
|
189
|
+
|
|
190
|
+
## Links
|
|
191
|
+
|
|
192
|
+
- **Docs and live playground** — <https://pdfcraft.dev>
|
|
193
|
+
- **Error reference** — <https://pdfcraft.dev/errors>
|
|
194
|
+
- **Pricing** — <https://pdfcraft.dev/pricing>
|
|
195
|
+
- **Status and support** — <mailto:support@pdfcraft.dev>
|
|
196
|
+
|
|
197
|
+
## Contributing
|
|
198
|
+
|
|
199
|
+
This repository is generated from the PDFCraft monorepo, where the client and the API share
|
|
200
|
+
one source of truth for option types and error codes. That means **pull requests here get
|
|
201
|
+
overwritten** on the next release.
|
|
202
|
+
|
|
203
|
+
Bug reports and API feedback are very welcome as issues — that is what this repo is for.
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT © Gaurav Singh. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Renderer = void 0;
|
|
4
|
+
const errors_js_1 = require("./errors.js");
|
|
5
|
+
const DEFAULTS = {
|
|
6
|
+
baseUrl: 'https://api.pdfcraft.dev',
|
|
7
|
+
maxRetries: 3,
|
|
8
|
+
timeoutMs: 130_000,
|
|
9
|
+
};
|
|
10
|
+
class Renderer {
|
|
11
|
+
apiKey;
|
|
12
|
+
baseUrl;
|
|
13
|
+
maxRetries;
|
|
14
|
+
timeoutMs;
|
|
15
|
+
doFetch;
|
|
16
|
+
constructor(apiKey, options = {}) {
|
|
17
|
+
if (!apiKey)
|
|
18
|
+
throw new errors_js_1.PDFCraftError('invalid_api_key', 'An API key is required.', 401);
|
|
19
|
+
this.apiKey = apiKey;
|
|
20
|
+
this.baseUrl = (options.baseUrl ?? DEFAULTS.baseUrl).replace(/\/+$/, '');
|
|
21
|
+
this.maxRetries = options.maxRetries ?? DEFAULTS.maxRetries;
|
|
22
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULTS.timeoutMs;
|
|
23
|
+
this.doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
24
|
+
}
|
|
25
|
+
/** The PDF bytes. In Node this is a Buffer, which is a Uint8Array. */
|
|
26
|
+
async render(input, idempotencyKey) {
|
|
27
|
+
const response = await this.send('/v1/render', { ...input, output: 'binary' }, idempotencyKey);
|
|
28
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
29
|
+
return typeof Buffer === 'undefined' ? bytes : Buffer.from(bytes);
|
|
30
|
+
}
|
|
31
|
+
/** Uploads the PDF and returns a signed link instead of the bytes. */
|
|
32
|
+
async renderToUrl(input, idempotencyKey) {
|
|
33
|
+
const response = await this.send('/v1/render', { ...input, output: 'url' }, idempotencyKey);
|
|
34
|
+
return (await response.json());
|
|
35
|
+
}
|
|
36
|
+
/** Queues the render and calls back when it settles. */
|
|
37
|
+
async renderAsync(input, idempotencyKey) {
|
|
38
|
+
const response = await this.send('/v1/render/async', input, idempotencyKey);
|
|
39
|
+
return (await response.json());
|
|
40
|
+
}
|
|
41
|
+
async getRender(id) {
|
|
42
|
+
const response = await this.request(`/v1/renders/${encodeURIComponent(id)}`, { method: 'GET' });
|
|
43
|
+
return (await response.json());
|
|
44
|
+
}
|
|
45
|
+
async usage() {
|
|
46
|
+
const response = await this.request('/v1/usage', { method: 'GET' });
|
|
47
|
+
return (await response.json());
|
|
48
|
+
}
|
|
49
|
+
send(path, body, idempotencyKey) {
|
|
50
|
+
return this.request(path, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'content-type': 'application/json',
|
|
54
|
+
...(idempotencyKey ? { 'idempotency-key': idempotencyKey } : {}),
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify(body),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async request(path, init) {
|
|
60
|
+
let lastError = null;
|
|
61
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
|
|
62
|
+
let response;
|
|
63
|
+
try {
|
|
64
|
+
response = await this.doFetch(`${this.baseUrl}${path}`, {
|
|
65
|
+
...init,
|
|
66
|
+
headers: {
|
|
67
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
68
|
+
'user-agent': 'pdfcraft-sdk-js/1.0.0',
|
|
69
|
+
...init.headers,
|
|
70
|
+
},
|
|
71
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (cause) {
|
|
75
|
+
lastError = new errors_js_1.PDFCraftError('network_error', `Could not reach PDFCraft: ${cause}`, 0);
|
|
76
|
+
if (attempt === this.maxRetries)
|
|
77
|
+
throw lastError;
|
|
78
|
+
await sleep(backoffMs(attempt));
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (response.ok)
|
|
82
|
+
return response;
|
|
83
|
+
const error = (0, errors_js_1.toError)(response.status, await response.text());
|
|
84
|
+
// 4xx other than 429 will fail identically no matter how many times we ask.
|
|
85
|
+
if (!error.retryable || attempt === this.maxRetries)
|
|
86
|
+
throw error;
|
|
87
|
+
lastError = error;
|
|
88
|
+
await sleep(retryAfterMs(response) ?? backoffMs(attempt));
|
|
89
|
+
}
|
|
90
|
+
throw lastError ?? new errors_js_1.PDFCraftError('internal_error', 'Request failed.', 500);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
exports.Renderer = Renderer;
|
|
94
|
+
function backoffMs(attempt) {
|
|
95
|
+
return Math.round(500 * 2 ** attempt * (0.75 + Math.random() * 0.5));
|
|
96
|
+
}
|
|
97
|
+
function retryAfterMs(response) {
|
|
98
|
+
const header = response.headers.get('retry-after');
|
|
99
|
+
if (!header)
|
|
100
|
+
return null;
|
|
101
|
+
const seconds = Number(header);
|
|
102
|
+
return Number.isFinite(seconds) ? Math.max(0, seconds) * 1000 : null;
|
|
103
|
+
}
|
|
104
|
+
function sleep(ms) {
|
|
105
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"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","sourcesContent":["// packages/sdk/src/client.ts\nimport type {\n AsyncRenderAccepted,\n AsyncRenderRequest,\n RenderRequest,\n RenderStatusResponse,\n RenderUrlResponse,\n UsageResponse,\n} from './contract/index.js';\nimport { PDFCraftError, toError } from './errors.js';\n\nexport interface RendererOptions {\n /** Override for testing or a self-hosted gateway. */\n baseUrl?: string;\n /** Retries on 429 and 5xx only. Default 3. */\n maxRetries?: number;\n /** Per-request timeout in milliseconds. Default 130000, just past the API's own ceiling. */\n timeoutMs?: number;\n fetch?: typeof globalThis.fetch;\n}\n\nconst DEFAULTS = {\n baseUrl: 'https://api.pdfcraft.dev',\n maxRetries: 3,\n timeoutMs: 130_000,\n};\n\n/**\n * The method you call decides the output mode, so passing `output` yourself\n * could only ever contradict it. Excluded from the type rather than ignored at\n * runtime: `render({ output: 'url' })` returning bytes is the kind of silent\n * surprise that costs someone an afternoon.\n */\nexport type RenderInput = Omit<RenderRequest, 'output'>;\nexport type AsyncRenderInput = Omit<AsyncRenderRequest, 'output'>;\n\nexport class Renderer {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly maxRetries: number;\n private readonly timeoutMs: number;\n private readonly doFetch: typeof globalThis.fetch;\n\n constructor(apiKey: string, options: RendererOptions = {}) {\n if (!apiKey) throw new PDFCraftError('invalid_api_key', 'An API key is required.', 401);\n this.apiKey = apiKey;\n this.baseUrl = (options.baseUrl ?? DEFAULTS.baseUrl).replace(/\\/+$/, '');\n this.maxRetries = options.maxRetries ?? DEFAULTS.maxRetries;\n this.timeoutMs = options.timeoutMs ?? DEFAULTS.timeoutMs;\n this.doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);\n }\n\n /** The PDF bytes. In Node this is a Buffer, which is a Uint8Array. */\n async render(input: RenderInput, idempotencyKey?: string): Promise<Uint8Array> {\n const response = await this.send('/v1/render', { ...input, output: 'binary' }, idempotencyKey);\n const bytes = new Uint8Array(await response.arrayBuffer());\n return typeof Buffer === 'undefined' ? bytes : Buffer.from(bytes);\n }\n\n /** Uploads the PDF and returns a signed link instead of the bytes. */\n async renderToUrl(input: RenderInput, idempotencyKey?: string): Promise<RenderUrlResponse> {\n const response = await this.send('/v1/render', { ...input, output: 'url' }, idempotencyKey);\n return (await response.json()) as RenderUrlResponse;\n }\n\n /** Queues the render and calls back when it settles. */\n async renderAsync(\n input: AsyncRenderInput,\n idempotencyKey?: string,\n ): Promise<AsyncRenderAccepted> {\n const response = await this.send('/v1/render/async', input, idempotencyKey);\n return (await response.json()) as AsyncRenderAccepted;\n }\n\n async getRender(id: string): Promise<RenderStatusResponse> {\n const response = await this.request(`/v1/renders/${encodeURIComponent(id)}`, { method: 'GET' });\n return (await response.json()) as RenderStatusResponse;\n }\n\n async usage(): Promise<UsageResponse> {\n const response = await this.request('/v1/usage', { method: 'GET' });\n return (await response.json()) as UsageResponse;\n }\n\n private send(path: string, body: unknown, idempotencyKey?: string): Promise<Response> {\n return this.request(path, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(idempotencyKey ? { 'idempotency-key': idempotencyKey } : {}),\n },\n body: JSON.stringify(body),\n });\n }\n\n private async request(path: string, init: RequestInit): Promise<Response> {\n let lastError: PDFCraftError | null = null;\n for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {\n let response: Response;\n try {\n response = await this.doFetch(`${this.baseUrl}${path}`, {\n ...init,\n headers: {\n authorization: `Bearer ${this.apiKey}`,\n 'user-agent': 'pdfcraft-sdk-js/1.0.0',\n ...(init.headers as Record<string, string> | undefined),\n },\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n } catch (cause) {\n lastError = new PDFCraftError('network_error', `Could not reach PDFCraft: ${cause}`, 0);\n if (attempt === this.maxRetries) throw lastError;\n await sleep(backoffMs(attempt));\n continue;\n }\n\n if (response.ok) return response;\n\n const error = toError(response.status, await response.text());\n // 4xx other than 429 will fail identically no matter how many times we ask.\n if (!error.retryable || attempt === this.maxRetries) throw error;\n lastError = error;\n await sleep(retryAfterMs(response) ?? backoffMs(attempt));\n }\n throw lastError ?? new PDFCraftError('internal_error', 'Request failed.', 500);\n }\n}\n\nfunction backoffMs(attempt: number): number {\n return Math.round(500 * 2 ** attempt * (0.75 + Math.random() * 0.5));\n}\n\nfunction retryAfterMs(response: Response): number | null {\n const header = response.headers.get('retry-after');\n if (!header) return null;\n const seconds = Number(header);\n return Number.isFinite(seconds) ? Math.max(0, seconds) * 1000 : null;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"]}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// packages/contract/src/errors.ts
|
|
3
|
+
//
|
|
4
|
+
// Every error the API can return. The exception filter maps domain errors onto
|
|
5
|
+
// this table, the SDK exposes `.code` from it, and the docs error reference is
|
|
6
|
+
// generated from it — each anchor matches the docs_url the API hands back.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.ERROR_SPEC_BY_CODE = exports.ERROR_TABLE = exports.ERROR_CODES = exports.DOCS_ORIGIN = void 0;
|
|
9
|
+
exports.docsUrlFor = docsUrlFor;
|
|
10
|
+
exports.DOCS_ORIGIN = 'https://pdfcraft.dev';
|
|
11
|
+
exports.ERROR_CODES = [
|
|
12
|
+
'invalid_request',
|
|
13
|
+
'invalid_api_key',
|
|
14
|
+
'payment_required',
|
|
15
|
+
'not_found',
|
|
16
|
+
'render_timeout',
|
|
17
|
+
'render_failed',
|
|
18
|
+
'rate_limited',
|
|
19
|
+
'quota_exceeded',
|
|
20
|
+
'internal_error',
|
|
21
|
+
];
|
|
22
|
+
exports.ERROR_TABLE = [
|
|
23
|
+
{
|
|
24
|
+
code: 'invalid_request',
|
|
25
|
+
status: 400,
|
|
26
|
+
billable: false,
|
|
27
|
+
when: 'Both html and url were supplied, or neither, or an option is out of range.',
|
|
28
|
+
whatToDo: 'Read the message — it names the offending field. Send exactly one of html or url.',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
code: 'invalid_api_key',
|
|
32
|
+
status: 401,
|
|
33
|
+
billable: false,
|
|
34
|
+
when: 'The Authorization header is missing, malformed, or the key has been revoked.',
|
|
35
|
+
whatToDo: 'Send "Authorization: Bearer sk_live_…". Issue a fresh key from the dashboard.',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
code: 'payment_required',
|
|
39
|
+
status: 402,
|
|
40
|
+
billable: false,
|
|
41
|
+
when: 'The last subscription payment failed, so the account is past_due.',
|
|
42
|
+
whatToDo: 'Update the card on the billing portal. Rendering resumes the moment payment clears.',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
code: 'not_found',
|
|
46
|
+
status: 404,
|
|
47
|
+
billable: false,
|
|
48
|
+
when: 'No render with that id belongs to your account.',
|
|
49
|
+
whatToDo: 'Check the id. Ids from another account also return 404, never 403, so they cannot be probed.',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
code: 'render_timeout',
|
|
53
|
+
status: 408,
|
|
54
|
+
billable: false,
|
|
55
|
+
when: 'The page did not finish within timeoutMs and was force-closed.',
|
|
56
|
+
whatToDo: 'Raise timeoutMs (max 120000), or relax waitFor — networkIdle on a page with a long-poll never settles.',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
code: 'render_failed',
|
|
60
|
+
status: 422,
|
|
61
|
+
billable: true,
|
|
62
|
+
when: 'Chromium ran but the page threw, navigation failed, or waitFor.selector never appeared.',
|
|
63
|
+
whatToDo: 'Load the same HTML in a browser. This one is billable because the browser did the work.',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
code: 'rate_limited',
|
|
67
|
+
status: 429,
|
|
68
|
+
billable: false,
|
|
69
|
+
when: 'More requests per second than the plan allows.',
|
|
70
|
+
whatToDo: 'Honour the Retry-After header and back off. Paid plans allow 20 req/s.',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
code: 'quota_exceeded',
|
|
74
|
+
status: 429,
|
|
75
|
+
billable: false,
|
|
76
|
+
when: 'A free-plan account has used all 100 renders in the current period.',
|
|
77
|
+
whatToDo: 'Upgrade, or wait for resets_at from GET /v1/usage. Paid plans never hard-stop; they accrue overage.',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
code: 'internal_error',
|
|
81
|
+
status: 500,
|
|
82
|
+
billable: false,
|
|
83
|
+
when: 'Something on our side broke.',
|
|
84
|
+
whatToDo: 'Retry with backoff. Never billed. If it persists, send us the render id.',
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
exports.ERROR_SPEC_BY_CODE = Object.fromEntries(exports.ERROR_TABLE.map((spec) => [spec.code, spec]));
|
|
88
|
+
function docsUrlFor(code) {
|
|
89
|
+
return `${exports.DOCS_ORIGIN}/errors#${code}`;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/contract/errors.ts"],"names":[],"mappings":";AAAA,kCAAkC;AAClC,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAC/E,2EAA2E;;;AAqG3E,gCAEC;AArGY,QAAA,WAAW,GAAG,sBAAsB,CAAC;AAErC,QAAA,WAAW,GAAG;IACzB,iBAAiB;IACjB,iBAAiB;IACjB,kBAAkB;IAClB,WAAW;IACX,gBAAgB;IAChB,eAAe;IACf,cAAc;IACd,gBAAgB;IAChB,gBAAgB;CACR,CAAC;AAaE,QAAA,WAAW,GAAG;IACzB;QACE,IAAI,EAAE,iBAAiB;QACvB,MAAM,EAAE,GAAG;QACX,QAAQ,EAAE,KAAK;QACf,IAAI,EAAE,4EAA4E;QAClF,QAAQ,EAAE,mFAAmF;KAC9F;IACD;QACE,IAAI,EAAE,iBAAiB;QACvB,MAAM,EAAE,GAAG;QACX,QAAQ,EAAE,KAAK;QACf,IAAI,EAAE,8EAA8E;QACpF,QAAQ,EAAE,+EAA+E;KAC1F;IACD;QACE,IAAI,EAAE,kBAAkB;QACxB,MAAM,EAAE,GAAG;QACX,QAAQ,EAAE,KAAK;QACf,IAAI,EAAE,mEAAmE;QACzE,QAAQ,EAAE,qFAAqF;KAChG;IACD;QACE,IAAI,EAAE,WAAW;QACjB,MAAM,EAAE,GAAG;QACX,QAAQ,EAAE,KAAK;QACf,IAAI,EAAE,iDAAiD;QACvD,QAAQ,EACN,8FAA8F;KACjG;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,MAAM,EAAE,GAAG;QACX,QAAQ,EAAE,KAAK;QACf,IAAI,EAAE,gEAAgE;QACtE,QAAQ,EACN,wGAAwG;KAC3G;IACD;QACE,IAAI,EAAE,eAAe;QACrB,MAAM,EAAE,GAAG;QACX,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,yFAAyF;QAC/F,QAAQ,EACN,yFAAyF;KAC5F;IACD;QACE,IAAI,EAAE,cAAc;QACpB,MAAM,EAAE,GAAG;QACX,QAAQ,EAAE,KAAK;QACf,IAAI,EAAE,gDAAgD;QACtD,QAAQ,EAAE,wEAAwE;KACnF;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,MAAM,EAAE,GAAG;QACX,QAAQ,EAAE,KAAK;QACf,IAAI,EAAE,qEAAqE;QAC3E,QAAQ,EACN,qGAAqG;KACxG;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,MAAM,EAAE,GAAG;QACX,QAAQ,EAAE,KAAK;QACf,IAAI,EAAE,8BAA8B;QACpC,QAAQ,EAAE,0EAA0E;KACrF;CACsC,CAAC;AAE7B,QAAA,kBAAkB,GAA2C,MAAM,CAAC,WAAW,CAC1F,mBAAW,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CACb,CAAC;AAElC,SAAgB,UAAU,CAAC,IAAe;IACxC,OAAO,GAAG,mBAAW,WAAW,IAAI,EAAE,CAAC;AACzC,CAAC","sourcesContent":["// packages/contract/src/errors.ts\n//\n// Every error the API can return. The exception filter maps domain errors onto\n// this table, the SDK exposes `.code` from it, and the docs error reference is\n// generated from it — each anchor matches the docs_url the API hands back.\n\nexport const DOCS_ORIGIN = 'https://pdfcraft.dev';\n\nexport const ERROR_CODES = [\n 'invalid_request',\n 'invalid_api_key',\n 'payment_required',\n 'not_found',\n 'render_timeout',\n 'render_failed',\n 'rate_limited',\n 'quota_exceeded',\n 'internal_error',\n] as const;\n\nexport type ErrorCode = (typeof ERROR_CODES)[number];\n\nexport interface ErrorSpec {\n readonly code: ErrorCode;\n readonly status: number;\n /** True when Chromium actually ran, which is what makes a render billable. */\n readonly billable: boolean;\n readonly when: string;\n readonly whatToDo: string;\n}\n\nexport const ERROR_TABLE = [\n {\n code: 'invalid_request',\n status: 400,\n billable: false,\n when: 'Both html and url were supplied, or neither, or an option is out of range.',\n whatToDo: 'Read the message — it names the offending field. Send exactly one of html or url.',\n },\n {\n code: 'invalid_api_key',\n status: 401,\n billable: false,\n when: 'The Authorization header is missing, malformed, or the key has been revoked.',\n whatToDo: 'Send \"Authorization: Bearer sk_live_…\". Issue a fresh key from the dashboard.',\n },\n {\n code: 'payment_required',\n status: 402,\n billable: false,\n when: 'The last subscription payment failed, so the account is past_due.',\n whatToDo: 'Update the card on the billing portal. Rendering resumes the moment payment clears.',\n },\n {\n code: 'not_found',\n status: 404,\n billable: false,\n when: 'No render with that id belongs to your account.',\n whatToDo:\n 'Check the id. Ids from another account also return 404, never 403, so they cannot be probed.',\n },\n {\n code: 'render_timeout',\n status: 408,\n billable: false,\n when: 'The page did not finish within timeoutMs and was force-closed.',\n whatToDo:\n 'Raise timeoutMs (max 120000), or relax waitFor — networkIdle on a page with a long-poll never settles.',\n },\n {\n code: 'render_failed',\n status: 422,\n billable: true,\n when: 'Chromium ran but the page threw, navigation failed, or waitFor.selector never appeared.',\n whatToDo:\n 'Load the same HTML in a browser. This one is billable because the browser did the work.',\n },\n {\n code: 'rate_limited',\n status: 429,\n billable: false,\n when: 'More requests per second than the plan allows.',\n whatToDo: 'Honour the Retry-After header and back off. Paid plans allow 20 req/s.',\n },\n {\n code: 'quota_exceeded',\n status: 429,\n billable: false,\n when: 'A free-plan account has used all 100 renders in the current period.',\n whatToDo:\n 'Upgrade, or wait for resets_at from GET /v1/usage. Paid plans never hard-stop; they accrue overage.',\n },\n {\n code: 'internal_error',\n status: 500,\n billable: false,\n when: 'Something on our side broke.',\n whatToDo: 'Retry with backoff. Never billed. If it persists, send us the render id.',\n },\n] as const satisfies readonly ErrorSpec[];\n\nexport const ERROR_SPEC_BY_CODE: Readonly<Record<ErrorCode, ErrorSpec>> = Object.fromEntries(\n ERROR_TABLE.map((spec) => [spec.code, spec]),\n) as Record<ErrorCode, ErrorSpec>;\n\nexport function docsUrlFor(code: ErrorCode): string {\n return `${DOCS_ORIGIN}/errors#${code}`;\n}\n\nexport interface ApiErrorBody {\n error: {\n code: ErrorCode;\n message: string;\n docs_url: string;\n };\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
// GENERATED FILE — do not edit.
|
|
18
|
+
// The same option, plan and error definitions the PDFCraft API validates
|
|
19
|
+
// requests against. Regenerated by packages/contract/scripts/sync-sdk.mjs.
|
|
20
|
+
__exportStar(require("./render-options.js"), exports);
|
|
21
|
+
__exportStar(require("./request.js"), exports);
|
|
22
|
+
__exportStar(require("./errors.js"), exports);
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/contract/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,gCAAgC;AAChC,yEAAyE;AACzE,2EAA2E;AAC3E,sDAAoC;AACpC,+CAA6B;AAC7B,8CAA4B","sourcesContent":["// GENERATED FILE — do not edit.\n// The same option, plan and error definitions the PDFCraft API validates\n// requests against. Regenerated by packages/contract/scripts/sync-sdk.mjs.\nexport * from './render-options.js';\nexport * from './request.js';\nexport * from './errors.js';\n"]}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// packages/contract/src/render-options.ts
|
|
3
|
+
//
|
|
4
|
+
// The one description of the render option surface. The API builds its validator
|
|
5
|
+
// from RENDER_OPTIONS_SPEC and the docs site builds its reference table from the
|
|
6
|
+
// same constant, so the two cannot drift. The SDK vendors this file verbatim at
|
|
7
|
+
// build time (see packages/sdk/scripts/vendor-contract.mjs) to stay dependency-free.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.SPEC_MATCHES_TYPES = exports.RENDER_OPTIONS_SPEC = exports.MAX_HTML_BYTES = exports.DEFAULT_TIMEOUT_MS = exports.MAX_TIMEOUT_MS = exports.EMULATE_MEDIA = exports.PAGE_FORMATS = void 0;
|
|
10
|
+
exports.PAGE_FORMATS = ['A4', 'A3', 'A5', 'Letter', 'Legal', 'Tabloid'];
|
|
11
|
+
exports.EMULATE_MEDIA = ['print', 'screen'];
|
|
12
|
+
exports.MAX_TIMEOUT_MS = 120_000;
|
|
13
|
+
exports.DEFAULT_TIMEOUT_MS = 30_000;
|
|
14
|
+
exports.MAX_HTML_BYTES = 5_000_000;
|
|
15
|
+
exports.RENDER_OPTIONS_SPEC = [
|
|
16
|
+
{
|
|
17
|
+
name: 'format',
|
|
18
|
+
type: { kind: 'enum', values: exports.PAGE_FORMATS },
|
|
19
|
+
default: 'A4',
|
|
20
|
+
description: 'Paper size. Ignored if the page CSS declares its own @page size.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'landscape',
|
|
24
|
+
type: { kind: 'boolean' },
|
|
25
|
+
default: false,
|
|
26
|
+
description: 'Rotate the paper to landscape orientation.',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'margin',
|
|
30
|
+
type: {
|
|
31
|
+
kind: 'object',
|
|
32
|
+
fields: [
|
|
33
|
+
{ name: 'top', type: { kind: 'css-length' }, description: 'e.g. "20mm", "1in", "72px".' },
|
|
34
|
+
{ name: 'right', type: { kind: 'css-length' }, description: 'e.g. "15mm".' },
|
|
35
|
+
{ name: 'bottom', type: { kind: 'css-length' }, description: 'e.g. "20mm".' },
|
|
36
|
+
{ name: 'left', type: { kind: 'css-length' }, description: 'e.g. "15mm".' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
description: 'Page margins. Any side may be omitted; omitted sides default to 0.',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'scale',
|
|
43
|
+
type: { kind: 'number', min: 0.1, max: 2 },
|
|
44
|
+
default: 1,
|
|
45
|
+
description: 'Rendering scale factor.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'printBackground',
|
|
49
|
+
type: { kind: 'boolean' },
|
|
50
|
+
default: true,
|
|
51
|
+
description: 'Print background colours and images. Off by default in browsers; on by default here.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'pageRanges',
|
|
55
|
+
type: { kind: 'string', maxLength: 100, pattern: '^[0-9,\\-\\s]+$' },
|
|
56
|
+
description: 'Subset of pages to keep, e.g. "1-5" or "1,4,7-9". Empty means all pages.',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'headerHtml',
|
|
60
|
+
type: { kind: 'string', maxLength: 50_000 },
|
|
61
|
+
description: 'HTML for the running header. Supports Chromium print classes: date, title, url, pageNumber, totalPages. Needs a top margin to be visible.',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'footerHtml',
|
|
65
|
+
type: { kind: 'string', maxLength: 50_000 },
|
|
66
|
+
description: 'HTML for the running footer. Same classes as headerHtml; needs a bottom margin.',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'waitFor',
|
|
70
|
+
type: {
|
|
71
|
+
kind: 'object',
|
|
72
|
+
fields: [
|
|
73
|
+
{
|
|
74
|
+
name: 'selector',
|
|
75
|
+
type: { kind: 'string', maxLength: 500 },
|
|
76
|
+
description: 'Wait until this CSS selector is attached to the DOM.',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'networkIdle',
|
|
80
|
+
type: { kind: 'boolean' },
|
|
81
|
+
default: false,
|
|
82
|
+
description: 'Wait until there have been no network connections for 500 ms.',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'delayMs',
|
|
86
|
+
type: { kind: 'integer', min: 0, max: 30_000 },
|
|
87
|
+
default: 0,
|
|
88
|
+
description: 'Fixed pause after the other wait conditions are satisfied.',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
description: 'Conditions to satisfy before the PDF is taken. All of them apply, in order.',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'emulateMedia',
|
|
96
|
+
type: { kind: 'enum', values: exports.EMULATE_MEDIA },
|
|
97
|
+
default: 'print',
|
|
98
|
+
description: 'Which CSS media type the page sees.',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'timeoutMs',
|
|
102
|
+
type: { kind: 'integer', min: 1000, max: exports.MAX_TIMEOUT_MS },
|
|
103
|
+
default: exports.DEFAULT_TIMEOUT_MS,
|
|
104
|
+
description: 'Hard ceiling on the whole render. Exceeding it returns 408 render_timeout.',
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
// Compile-time drift guard: this assignment stops type-checking the moment
|
|
108
|
+
// RENDER_OPTIONS_SPEC and RenderOptions describe different sets of options.
|
|
109
|
+
const driftCheck = true;
|
|
110
|
+
exports.SPEC_MATCHES_TYPES = driftCheck;
|
|
111
|
+
//# sourceMappingURL=render-options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-options.js","sourceRoot":"","sources":["../../../src/contract/render-options.ts"],"names":[],"mappings":"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","sourcesContent":["// packages/contract/src/render-options.ts\n//\n// The one description of the render option surface. The API builds its validator\n// from RENDER_OPTIONS_SPEC and the docs site builds its reference table from the\n// same constant, so the two cannot drift. The SDK vendors this file verbatim at\n// build time (see packages/sdk/scripts/vendor-contract.mjs) to stay dependency-free.\n\nexport type OptionKind =\n | { readonly kind: 'enum'; readonly values: readonly string[] }\n | { readonly kind: 'boolean' }\n | { readonly kind: 'number'; readonly min: number; readonly max: number }\n | { readonly kind: 'integer'; readonly min: number; readonly max: number }\n | { readonly kind: 'string'; readonly maxLength: number; readonly pattern?: string }\n | { readonly kind: 'css-length' }\n | { readonly kind: 'object'; readonly fields: readonly OptionField[] };\n\nexport interface OptionField {\n readonly name: string;\n readonly type: OptionKind;\n readonly default?: string | number | boolean;\n readonly description: string;\n}\n\nexport const PAGE_FORMATS = ['A4', 'A3', 'A5', 'Letter', 'Legal', 'Tabloid'] as const;\nexport const EMULATE_MEDIA = ['print', 'screen'] as const;\n\nexport const MAX_TIMEOUT_MS = 120_000;\nexport const DEFAULT_TIMEOUT_MS = 30_000;\nexport const MAX_HTML_BYTES = 5_000_000;\n\nexport const RENDER_OPTIONS_SPEC = [\n {\n name: 'format',\n type: { kind: 'enum', values: PAGE_FORMATS },\n default: 'A4',\n description: 'Paper size. Ignored if the page CSS declares its own @page size.',\n },\n {\n name: 'landscape',\n type: { kind: 'boolean' },\n default: false,\n description: 'Rotate the paper to landscape orientation.',\n },\n {\n name: 'margin',\n type: {\n kind: 'object',\n fields: [\n { name: 'top', type: { kind: 'css-length' }, description: 'e.g. \"20mm\", \"1in\", \"72px\".' },\n { name: 'right', type: { kind: 'css-length' }, description: 'e.g. \"15mm\".' },\n { name: 'bottom', type: { kind: 'css-length' }, description: 'e.g. \"20mm\".' },\n { name: 'left', type: { kind: 'css-length' }, description: 'e.g. \"15mm\".' },\n ],\n },\n description: 'Page margins. Any side may be omitted; omitted sides default to 0.',\n },\n {\n name: 'scale',\n type: { kind: 'number', min: 0.1, max: 2 },\n default: 1,\n description: 'Rendering scale factor.',\n },\n {\n name: 'printBackground',\n type: { kind: 'boolean' },\n default: true,\n description:\n 'Print background colours and images. Off by default in browsers; on by default here.',\n },\n {\n name: 'pageRanges',\n type: { kind: 'string', maxLength: 100, pattern: '^[0-9,\\\\-\\\\s]+$' },\n description: 'Subset of pages to keep, e.g. \"1-5\" or \"1,4,7-9\". Empty means all pages.',\n },\n {\n name: 'headerHtml',\n type: { kind: 'string', maxLength: 50_000 },\n description:\n 'HTML for the running header. Supports Chromium print classes: date, title, url, pageNumber, totalPages. Needs a top margin to be visible.',\n },\n {\n name: 'footerHtml',\n type: { kind: 'string', maxLength: 50_000 },\n description: 'HTML for the running footer. Same classes as headerHtml; needs a bottom margin.',\n },\n {\n name: 'waitFor',\n type: {\n kind: 'object',\n fields: [\n {\n name: 'selector',\n type: { kind: 'string', maxLength: 500 },\n description: 'Wait until this CSS selector is attached to the DOM.',\n },\n {\n name: 'networkIdle',\n type: { kind: 'boolean' },\n default: false,\n description: 'Wait until there have been no network connections for 500 ms.',\n },\n {\n name: 'delayMs',\n type: { kind: 'integer', min: 0, max: 30_000 },\n default: 0,\n description: 'Fixed pause after the other wait conditions are satisfied.',\n },\n ],\n },\n description: 'Conditions to satisfy before the PDF is taken. All of them apply, in order.',\n },\n {\n name: 'emulateMedia',\n type: { kind: 'enum', values: EMULATE_MEDIA },\n default: 'print',\n description: 'Which CSS media type the page sees.',\n },\n {\n name: 'timeoutMs',\n type: { kind: 'integer', min: 1000, max: MAX_TIMEOUT_MS },\n default: DEFAULT_TIMEOUT_MS,\n description: 'Hard ceiling on the whole render. Exceeding it returns 408 render_timeout.',\n },\n] as const satisfies readonly OptionField[];\n\nexport interface Margin {\n top?: string;\n right?: string;\n bottom?: string;\n left?: string;\n}\n\nexport interface WaitFor {\n selector?: string;\n networkIdle?: boolean;\n delayMs?: number;\n}\n\nexport interface RenderOptions {\n format?: (typeof PAGE_FORMATS)[number];\n landscape?: boolean;\n margin?: Margin;\n scale?: number;\n printBackground?: boolean;\n pageRanges?: string;\n headerHtml?: string;\n footerHtml?: string;\n waitFor?: WaitFor;\n emulateMedia?: (typeof EMULATE_MEDIA)[number];\n timeoutMs?: number;\n}\n\ntype SpecFieldName = (typeof RENDER_OPTIONS_SPEC)[number]['name'];\n\n// Compile-time drift guard: this assignment stops type-checking the moment\n// RENDER_OPTIONS_SPEC and RenderOptions describe different sets of options.\nconst driftCheck: [Exclude<keyof RenderOptions, SpecFieldName>] extends [never]\n ? [Exclude<SpecFieldName, keyof RenderOptions>] extends [never]\n ? true\n : never\n : never = true;\n\nexport const SPEC_MATCHES_TYPES: boolean = driftCheck;\n"]}
|