@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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +207 -0
  3. package/dist/cjs/client.js +107 -0
  4. package/dist/cjs/client.js.map +1 -0
  5. package/dist/cjs/contract/errors.js +91 -0
  6. package/dist/cjs/contract/errors.js.map +1 -0
  7. package/dist/cjs/contract/index.js +23 -0
  8. package/dist/cjs/contract/index.js.map +1 -0
  9. package/dist/cjs/contract/render-options.js +111 -0
  10. package/dist/cjs/contract/render-options.js.map +1 -0
  11. package/dist/cjs/contract/request.js +43 -0
  12. package/dist/cjs/contract/request.js.map +1 -0
  13. package/dist/cjs/errors.js +31 -0
  14. package/dist/cjs/errors.js.map +1 -0
  15. package/dist/cjs/index.js +9 -0
  16. package/dist/cjs/index.js.map +1 -0
  17. package/dist/cjs/package.json +1 -0
  18. package/dist/esm/client.d.ts +36 -0
  19. package/dist/esm/client.js +103 -0
  20. package/dist/esm/client.js.map +1 -0
  21. package/dist/esm/contract/errors.d.ts +75 -0
  22. package/dist/esm/contract/errors.js +87 -0
  23. package/dist/esm/contract/errors.js.map +1 -0
  24. package/dist/esm/contract/index.d.ts +3 -0
  25. package/dist/esm/contract/index.js +7 -0
  26. package/dist/esm/contract/index.js.map +1 -0
  27. package/dist/esm/contract/render-options.d.ts +191 -0
  28. package/dist/esm/contract/render-options.js +108 -0
  29. package/dist/esm/contract/render-options.js.map +1 -0
  30. package/dist/esm/contract/request.d.ts +121 -0
  31. package/dist/esm/contract/request.js +40 -0
  32. package/dist/esm/contract/request.js.map +1 -0
  33. package/dist/esm/errors.d.ts +10 -0
  34. package/dist/esm/errors.js +26 -0
  35. package/dist/esm/errors.js.map +1 -0
  36. package/dist/esm/index.d.ts +3 -0
  37. package/dist/esm/index.js +4 -0
  38. package/dist/esm/index.js.map +1 -0
  39. package/dist/esm/package.json +1 -0
  40. package/package.json +60 -0
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REQUEST_FIELDS_SPEC = exports.ACCOUNT_STATUSES = exports.RENDER_SOURCES = exports.RENDER_STATUSES = exports.OUTPUT_MODES = void 0;
4
+ exports.OUTPUT_MODES = ['binary', 'url'];
5
+ exports.RENDER_STATUSES = ['queued', 'rendering', 'succeeded', 'failed'];
6
+ exports.RENDER_SOURCES = ['html', 'url'];
7
+ exports.ACCOUNT_STATUSES = ['active', 'past_due', 'cancelled'];
8
+ /** Top-level request fields, for the generated docs reference. */
9
+ exports.REQUEST_FIELDS_SPEC = [
10
+ {
11
+ name: 'html',
12
+ type: { kind: 'string', maxLength: 5_000_000 },
13
+ description: 'The document to render. Mutually exclusive with url; exactly one is required.',
14
+ },
15
+ {
16
+ name: 'url',
17
+ type: { kind: 'string', maxLength: 2000 },
18
+ description: 'A public http(s) URL to render. Mutually exclusive with html.',
19
+ },
20
+ {
21
+ name: 'output',
22
+ type: { kind: 'enum', values: exports.OUTPUT_MODES },
23
+ default: 'binary',
24
+ description: '"binary" streams application/pdf back; "url" uploads and returns a signed link.',
25
+ },
26
+ {
27
+ name: 'filename',
28
+ type: { kind: 'string', maxLength: 255 },
29
+ default: 'document.pdf',
30
+ description: 'Used for Content-Disposition on binary output and in the signed URL.',
31
+ },
32
+ {
33
+ name: 'headers',
34
+ type: { kind: 'object', fields: [] },
35
+ description: 'url input only. Extra request headers, e.g. an Authorization header.',
36
+ },
37
+ {
38
+ name: 'cookies',
39
+ type: { kind: 'object', fields: [] },
40
+ description: 'url input only. Array of {name, value, domain?, path?} seeded into the context.',
41
+ },
42
+ ];
43
+ //# sourceMappingURL=request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../../../src/contract/request.ts"],"names":[],"mappings":";;;AAGa,QAAA,YAAY,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAU,CAAC;AAG1C,QAAA,eAAe,GAAG,CAAC,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,CAAU,CAAC;AAG1E,QAAA,cAAc,GAAG,CAAC,MAAM,EAAE,KAAK,CAAU,CAAC;AAG1C,QAAA,gBAAgB,GAAG,CAAC,QAAQ,EAAE,UAAU,EAAE,WAAW,CAAU,CAAC;AAuE7E,kEAAkE;AACrD,QAAA,mBAAmB,GAAG;IACjC;QACE,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE;QAC9C,WAAW,EAAE,+EAA+E;KAC7F;IACD;QACE,IAAI,EAAE,KAAK;QACX,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE;QACzC,WAAW,EAAE,+DAA+D;KAC7E;IACD;QACE,IAAI,EAAE,QAAQ;QACd,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAY,EAAE;QAC5C,OAAO,EAAE,QAAQ;QACjB,WAAW,EAAE,iFAAiF;KAC/F;IACD;QACE,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,EAAE;QACxC,OAAO,EAAE,cAAc;QACvB,WAAW,EAAE,sEAAsE;KACpF;IACD;QACE,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE;QACpC,WAAW,EAAE,sEAAsE;KACpF;IACD;QACE,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE;QACpC,WAAW,EAAE,iFAAiF;KAC/F;CACwC,CAAC","sourcesContent":["// packages/contract/src/request.ts\nimport { type OptionField, type RenderOptions } from './render-options.js';\n\nexport const OUTPUT_MODES = ['binary', 'url'] as const;\nexport type OutputMode = (typeof OUTPUT_MODES)[number];\n\nexport const RENDER_STATUSES = ['queued', 'rendering', 'succeeded', 'failed'] as const;\nexport type RenderStatus = (typeof RENDER_STATUSES)[number];\n\nexport const RENDER_SOURCES = ['html', 'url'] as const;\nexport type RenderSource = (typeof RENDER_SOURCES)[number];\n\nexport const ACCOUNT_STATUSES = ['active', 'past_due', 'cancelled'] as const;\nexport type AccountStatus = (typeof ACCOUNT_STATUSES)[number];\n\nexport interface Cookie {\n name: string;\n value: string;\n domain?: string;\n path?: string;\n}\n\nexport interface RenderRequest {\n /** Exactly one of html or url. */\n html?: string;\n url?: string;\n options?: RenderOptions;\n output?: OutputMode;\n filename?: string;\n /** url input only: extra request headers, so an authenticated page can be rendered. */\n headers?: Record<string, string>;\n /** url input only: cookies to seed the context with. */\n cookies?: Cookie[];\n}\n\nexport interface AsyncRenderRequest extends RenderRequest {\n callback_url: string;\n}\n\nexport interface RenderUrlResponse {\n id: string;\n url: string;\n expires_at: string;\n pages: number;\n bytes: number;\n duration_ms: number;\n}\n\nexport interface AsyncRenderAccepted {\n id: string;\n status: 'queued';\n}\n\nexport interface RenderStatusResponse {\n id: string;\n status: RenderStatus;\n url?: string;\n expires_at?: string;\n pages?: number;\n bytes?: number;\n duration_ms?: number;\n error?: { code: string; message: string };\n}\n\nexport interface UsageResponse {\n used: number;\n limit: number;\n overage_count: number;\n period_start: string;\n resets_at: string;\n}\n\n/** Body sent to callback_url when an async render settles. */\nexport interface WebhookPayload {\n id: string;\n status: 'succeeded' | 'failed';\n url?: string;\n pages?: number;\n bytes?: number;\n duration_ms: number;\n error?: { code: string; message: string };\n}\n\n/** Top-level request fields, for the generated docs reference. */\nexport const REQUEST_FIELDS_SPEC = [\n {\n name: 'html',\n type: { kind: 'string', maxLength: 5_000_000 },\n description: 'The document to render. Mutually exclusive with url; exactly one is required.',\n },\n {\n name: 'url',\n type: { kind: 'string', maxLength: 2000 },\n description: 'A public http(s) URL to render. Mutually exclusive with html.',\n },\n {\n name: 'output',\n type: { kind: 'enum', values: OUTPUT_MODES },\n default: 'binary',\n description: '\"binary\" streams application/pdf back; \"url\" uploads and returns a signed link.',\n },\n {\n name: 'filename',\n type: { kind: 'string', maxLength: 255 },\n default: 'document.pdf',\n description: 'Used for Content-Disposition on binary output and in the signed URL.',\n },\n {\n name: 'headers',\n type: { kind: 'object', fields: [] },\n description: 'url input only. Extra request headers, e.g. an Authorization header.',\n },\n {\n name: 'cookies',\n type: { kind: 'object', fields: [] },\n description: 'url input only. Array of {name, value, domain?, path?} seeded into the context.',\n },\n] as const satisfies readonly OptionField[];\n"]}
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PDFCraftError = void 0;
4
+ exports.toError = toError;
5
+ /** Every failure from the API arrives as one of these. Branch on `.code`. */
6
+ class PDFCraftError extends Error {
7
+ code;
8
+ status;
9
+ docsUrl;
10
+ retryable;
11
+ constructor(code, message, status, docsUrl = null) {
12
+ super(message);
13
+ this.name = 'PDFCraftError';
14
+ this.code = code;
15
+ this.status = status;
16
+ this.docsUrl = docsUrl;
17
+ this.retryable = code === 'network_error' || status === 429 || status >= 500;
18
+ }
19
+ }
20
+ exports.PDFCraftError = PDFCraftError;
21
+ function toError(status, body) {
22
+ let parsed = {};
23
+ try {
24
+ parsed = JSON.parse(body);
25
+ }
26
+ catch {
27
+ // Non-JSON body (a proxy error page, say) — fall through to the defaults.
28
+ }
29
+ return new PDFCraftError(parsed.error?.code ?? 'internal_error', parsed.error?.message ?? `PDFCraft responded ${status}`, status, parsed.error?.docs_url ?? null);
30
+ }
31
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":";;;AA6BA,0BAaC;AAvCD,6EAA6E;AAC7E,MAAa,aAAc,SAAQ,KAAK;IAC7B,IAAI,CAA8B;IAClC,MAAM,CAAS;IACf,OAAO,CAAgB;IACvB,SAAS,CAAU;IAE5B,YACE,IAAiC,EACjC,OAAe,EACf,MAAc,EACd,UAAyB,IAAI;QAE7B,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,SAAS,GAAG,IAAI,KAAK,eAAe,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,IAAI,GAAG,CAAC;IAC/E,CAAC;CACF;AAnBD,sCAmBC;AAMD,SAAgB,OAAO,CAAC,MAAc,EAAE,IAAY;IAClD,IAAI,MAAM,GAAc,EAAE,CAAC;IAC3B,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAc,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,0EAA0E;IAC5E,CAAC;IACD,OAAO,IAAI,aAAa,CACrB,MAAM,CAAC,KAAK,EAAE,IAAkB,IAAI,gBAAgB,EACrD,MAAM,CAAC,KAAK,EAAE,OAAO,IAAI,sBAAsB,MAAM,EAAE,EACvD,MAAM,EACN,MAAM,CAAC,KAAK,EAAE,QAAQ,IAAI,IAAI,CAC/B,CAAC;AACJ,CAAC","sourcesContent":["// packages/sdk/src/errors.ts\nimport type { ErrorCode } from './contract/index.js';\n\n/** Every failure from the API arrives as one of these. Branch on `.code`. */\nexport class PDFCraftError extends Error {\n readonly code: ErrorCode | 'network_error';\n readonly status: number;\n readonly docsUrl: string | null;\n readonly retryable: boolean;\n\n constructor(\n code: ErrorCode | 'network_error',\n message: string,\n status: number,\n docsUrl: string | null = null,\n ) {\n super(message);\n this.name = 'PDFCraftError';\n this.code = code;\n this.status = status;\n this.docsUrl = docsUrl;\n this.retryable = code === 'network_error' || status === 429 || status >= 500;\n }\n}\n\ninterface WireError {\n error?: { code?: string; message?: string; docs_url?: string };\n}\n\nexport function toError(status: number, body: string): PDFCraftError {\n let parsed: WireError = {};\n try {\n parsed = JSON.parse(body) as WireError;\n } catch {\n // Non-JSON body (a proxy error page, say) — fall through to the defaults.\n }\n return new PDFCraftError(\n (parsed.error?.code as ErrorCode) ?? 'internal_error',\n parsed.error?.message ?? `PDFCraft responded ${status}`,\n status,\n parsed.error?.docs_url ?? null,\n );\n}\n"]}
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PDFCraftError = exports.Renderer = void 0;
4
+ // packages/sdk/src/index.ts
5
+ var client_js_1 = require("./client.js");
6
+ Object.defineProperty(exports, "Renderer", { enumerable: true, get: function () { return client_js_1.Renderer; } });
7
+ var errors_js_1 = require("./errors.js");
8
+ Object.defineProperty(exports, "PDFCraftError", { enumerable: true, get: function () { return errors_js_1.PDFCraftError; } });
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;AAAA,4BAA4B;AAC5B,yCAKqB;AAJnB,qGAAA,QAAQ,OAAA;AAKV,yCAA4C;AAAnC,0GAAA,aAAa,OAAA","sourcesContent":["// packages/sdk/src/index.ts\nexport {\n Renderer,\n type AsyncRenderInput,\n type RenderInput,\n type RendererOptions,\n} from './client.js';\nexport { PDFCraftError } from './errors.js';\nexport type {\n AsyncRenderAccepted,\n AsyncRenderRequest,\n Cookie,\n ErrorCode,\n Margin,\n OutputMode,\n RenderOptions,\n RenderRequest,\n RenderStatus,\n RenderStatusResponse,\n RenderUrlResponse,\n UsageResponse,\n WaitFor,\n WebhookPayload,\n} from './contract/index.js';\n"]}
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,36 @@
1
+ import type { AsyncRenderAccepted, AsyncRenderRequest, RenderRequest, RenderStatusResponse, RenderUrlResponse, UsageResponse } from './contract/index.js';
2
+ export interface RendererOptions {
3
+ /** Override for testing or a self-hosted gateway. */
4
+ baseUrl?: string;
5
+ /** Retries on 429 and 5xx only. Default 3. */
6
+ maxRetries?: number;
7
+ /** Per-request timeout in milliseconds. Default 130000, just past the API's own ceiling. */
8
+ timeoutMs?: number;
9
+ fetch?: typeof globalThis.fetch;
10
+ }
11
+ /**
12
+ * The method you call decides the output mode, so passing `output` yourself
13
+ * could only ever contradict it. Excluded from the type rather than ignored at
14
+ * runtime: `render({ output: 'url' })` returning bytes is the kind of silent
15
+ * surprise that costs someone an afternoon.
16
+ */
17
+ export type RenderInput = Omit<RenderRequest, 'output'>;
18
+ export type AsyncRenderInput = Omit<AsyncRenderRequest, 'output'>;
19
+ export declare class Renderer {
20
+ private readonly apiKey;
21
+ private readonly baseUrl;
22
+ private readonly maxRetries;
23
+ private readonly timeoutMs;
24
+ private readonly doFetch;
25
+ constructor(apiKey: string, options?: RendererOptions);
26
+ /** The PDF bytes. In Node this is a Buffer, which is a Uint8Array. */
27
+ render(input: RenderInput, idempotencyKey?: string): Promise<Uint8Array>;
28
+ /** Uploads the PDF and returns a signed link instead of the bytes. */
29
+ renderToUrl(input: RenderInput, idempotencyKey?: string): Promise<RenderUrlResponse>;
30
+ /** Queues the render and calls back when it settles. */
31
+ renderAsync(input: AsyncRenderInput, idempotencyKey?: string): Promise<AsyncRenderAccepted>;
32
+ getRender(id: string): Promise<RenderStatusResponse>;
33
+ usage(): Promise<UsageResponse>;
34
+ private send;
35
+ private request;
36
+ }
@@ -0,0 +1,103 @@
1
+ import { PDFCraftError, toError } from './errors.js';
2
+ const DEFAULTS = {
3
+ baseUrl: 'https://api.pdfcraft.dev',
4
+ maxRetries: 3,
5
+ timeoutMs: 130_000,
6
+ };
7
+ export class Renderer {
8
+ apiKey;
9
+ baseUrl;
10
+ maxRetries;
11
+ timeoutMs;
12
+ doFetch;
13
+ constructor(apiKey, options = {}) {
14
+ if (!apiKey)
15
+ throw new PDFCraftError('invalid_api_key', 'An API key is required.', 401);
16
+ this.apiKey = apiKey;
17
+ this.baseUrl = (options.baseUrl ?? DEFAULTS.baseUrl).replace(/\/+$/, '');
18
+ this.maxRetries = options.maxRetries ?? DEFAULTS.maxRetries;
19
+ this.timeoutMs = options.timeoutMs ?? DEFAULTS.timeoutMs;
20
+ this.doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
21
+ }
22
+ /** The PDF bytes. In Node this is a Buffer, which is a Uint8Array. */
23
+ async render(input, idempotencyKey) {
24
+ const response = await this.send('/v1/render', { ...input, output: 'binary' }, idempotencyKey);
25
+ const bytes = new Uint8Array(await response.arrayBuffer());
26
+ return typeof Buffer === 'undefined' ? bytes : Buffer.from(bytes);
27
+ }
28
+ /** Uploads the PDF and returns a signed link instead of the bytes. */
29
+ async renderToUrl(input, idempotencyKey) {
30
+ const response = await this.send('/v1/render', { ...input, output: 'url' }, idempotencyKey);
31
+ return (await response.json());
32
+ }
33
+ /** Queues the render and calls back when it settles. */
34
+ async renderAsync(input, idempotencyKey) {
35
+ const response = await this.send('/v1/render/async', input, idempotencyKey);
36
+ return (await response.json());
37
+ }
38
+ async getRender(id) {
39
+ const response = await this.request(`/v1/renders/${encodeURIComponent(id)}`, { method: 'GET' });
40
+ return (await response.json());
41
+ }
42
+ async usage() {
43
+ const response = await this.request('/v1/usage', { method: 'GET' });
44
+ return (await response.json());
45
+ }
46
+ send(path, body, idempotencyKey) {
47
+ return this.request(path, {
48
+ method: 'POST',
49
+ headers: {
50
+ 'content-type': 'application/json',
51
+ ...(idempotencyKey ? { 'idempotency-key': idempotencyKey } : {}),
52
+ },
53
+ body: JSON.stringify(body),
54
+ });
55
+ }
56
+ async request(path, init) {
57
+ let lastError = null;
58
+ for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
59
+ let response;
60
+ try {
61
+ response = await this.doFetch(`${this.baseUrl}${path}`, {
62
+ ...init,
63
+ headers: {
64
+ authorization: `Bearer ${this.apiKey}`,
65
+ 'user-agent': 'pdfcraft-sdk-js/1.0.0',
66
+ ...init.headers,
67
+ },
68
+ signal: AbortSignal.timeout(this.timeoutMs),
69
+ });
70
+ }
71
+ catch (cause) {
72
+ lastError = new PDFCraftError('network_error', `Could not reach PDFCraft: ${cause}`, 0);
73
+ if (attempt === this.maxRetries)
74
+ throw lastError;
75
+ await sleep(backoffMs(attempt));
76
+ continue;
77
+ }
78
+ if (response.ok)
79
+ return response;
80
+ const error = toError(response.status, await response.text());
81
+ // 4xx other than 429 will fail identically no matter how many times we ask.
82
+ if (!error.retryable || attempt === this.maxRetries)
83
+ throw error;
84
+ lastError = error;
85
+ await sleep(retryAfterMs(response) ?? backoffMs(attempt));
86
+ }
87
+ throw lastError ?? new PDFCraftError('internal_error', 'Request failed.', 500);
88
+ }
89
+ }
90
+ function backoffMs(attempt) {
91
+ return Math.round(500 * 2 ** attempt * (0.75 + Math.random() * 0.5));
92
+ }
93
+ function retryAfterMs(response) {
94
+ const header = response.headers.get('retry-after');
95
+ if (!header)
96
+ return null;
97
+ const seconds = Number(header);
98
+ return Number.isFinite(seconds) ? Math.max(0, seconds) * 1000 : null;
99
+ }
100
+ function sleep(ms) {
101
+ return new Promise((resolve) => setTimeout(resolve, ms));
102
+ }
103
+ //# 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,75 @@
1
+ export declare const DOCS_ORIGIN = "https://pdfcraft.dev";
2
+ export declare const ERROR_CODES: readonly ["invalid_request", "invalid_api_key", "payment_required", "not_found", "render_timeout", "render_failed", "rate_limited", "quota_exceeded", "internal_error"];
3
+ export type ErrorCode = (typeof ERROR_CODES)[number];
4
+ export interface ErrorSpec {
5
+ readonly code: ErrorCode;
6
+ readonly status: number;
7
+ /** True when Chromium actually ran, which is what makes a render billable. */
8
+ readonly billable: boolean;
9
+ readonly when: string;
10
+ readonly whatToDo: string;
11
+ }
12
+ export declare const ERROR_TABLE: readonly [{
13
+ readonly code: "invalid_request";
14
+ readonly status: 400;
15
+ readonly billable: false;
16
+ readonly when: "Both html and url were supplied, or neither, or an option is out of range.";
17
+ readonly whatToDo: "Read the message — it names the offending field. Send exactly one of html or url.";
18
+ }, {
19
+ readonly code: "invalid_api_key";
20
+ readonly status: 401;
21
+ readonly billable: false;
22
+ readonly when: "The Authorization header is missing, malformed, or the key has been revoked.";
23
+ readonly whatToDo: "Send \"Authorization: Bearer sk_live_…\". Issue a fresh key from the dashboard.";
24
+ }, {
25
+ readonly code: "payment_required";
26
+ readonly status: 402;
27
+ readonly billable: false;
28
+ readonly when: "The last subscription payment failed, so the account is past_due.";
29
+ readonly whatToDo: "Update the card on the billing portal. Rendering resumes the moment payment clears.";
30
+ }, {
31
+ readonly code: "not_found";
32
+ readonly status: 404;
33
+ readonly billable: false;
34
+ readonly when: "No render with that id belongs to your account.";
35
+ readonly whatToDo: "Check the id. Ids from another account also return 404, never 403, so they cannot be probed.";
36
+ }, {
37
+ readonly code: "render_timeout";
38
+ readonly status: 408;
39
+ readonly billable: false;
40
+ readonly when: "The page did not finish within timeoutMs and was force-closed.";
41
+ readonly whatToDo: "Raise timeoutMs (max 120000), or relax waitFor — networkIdle on a page with a long-poll never settles.";
42
+ }, {
43
+ readonly code: "render_failed";
44
+ readonly status: 422;
45
+ readonly billable: true;
46
+ readonly when: "Chromium ran but the page threw, navigation failed, or waitFor.selector never appeared.";
47
+ readonly whatToDo: "Load the same HTML in a browser. This one is billable because the browser did the work.";
48
+ }, {
49
+ readonly code: "rate_limited";
50
+ readonly status: 429;
51
+ readonly billable: false;
52
+ readonly when: "More requests per second than the plan allows.";
53
+ readonly whatToDo: "Honour the Retry-After header and back off. Paid plans allow 20 req/s.";
54
+ }, {
55
+ readonly code: "quota_exceeded";
56
+ readonly status: 429;
57
+ readonly billable: false;
58
+ readonly when: "A free-plan account has used all 100 renders in the current period.";
59
+ readonly whatToDo: "Upgrade, or wait for resets_at from GET /v1/usage. Paid plans never hard-stop; they accrue overage.";
60
+ }, {
61
+ readonly code: "internal_error";
62
+ readonly status: 500;
63
+ readonly billable: false;
64
+ readonly when: "Something on our side broke.";
65
+ readonly whatToDo: "Retry with backoff. Never billed. If it persists, send us the render id.";
66
+ }];
67
+ export declare const ERROR_SPEC_BY_CODE: Readonly<Record<ErrorCode, ErrorSpec>>;
68
+ export declare function docsUrlFor(code: ErrorCode): string;
69
+ export interface ApiErrorBody {
70
+ error: {
71
+ code: ErrorCode;
72
+ message: string;
73
+ docs_url: string;
74
+ };
75
+ }
@@ -0,0 +1,87 @@
1
+ // packages/contract/src/errors.ts
2
+ //
3
+ // Every error the API can return. The exception filter maps domain errors onto
4
+ // this table, the SDK exposes `.code` from it, and the docs error reference is
5
+ // generated from it — each anchor matches the docs_url the API hands back.
6
+ export const DOCS_ORIGIN = 'https://pdfcraft.dev';
7
+ export const ERROR_CODES = [
8
+ 'invalid_request',
9
+ 'invalid_api_key',
10
+ 'payment_required',
11
+ 'not_found',
12
+ 'render_timeout',
13
+ 'render_failed',
14
+ 'rate_limited',
15
+ 'quota_exceeded',
16
+ 'internal_error',
17
+ ];
18
+ export const ERROR_TABLE = [
19
+ {
20
+ code: 'invalid_request',
21
+ status: 400,
22
+ billable: false,
23
+ when: 'Both html and url were supplied, or neither, or an option is out of range.',
24
+ whatToDo: 'Read the message — it names the offending field. Send exactly one of html or url.',
25
+ },
26
+ {
27
+ code: 'invalid_api_key',
28
+ status: 401,
29
+ billable: false,
30
+ when: 'The Authorization header is missing, malformed, or the key has been revoked.',
31
+ whatToDo: 'Send "Authorization: Bearer sk_live_…". Issue a fresh key from the dashboard.',
32
+ },
33
+ {
34
+ code: 'payment_required',
35
+ status: 402,
36
+ billable: false,
37
+ when: 'The last subscription payment failed, so the account is past_due.',
38
+ whatToDo: 'Update the card on the billing portal. Rendering resumes the moment payment clears.',
39
+ },
40
+ {
41
+ code: 'not_found',
42
+ status: 404,
43
+ billable: false,
44
+ when: 'No render with that id belongs to your account.',
45
+ whatToDo: 'Check the id. Ids from another account also return 404, never 403, so they cannot be probed.',
46
+ },
47
+ {
48
+ code: 'render_timeout',
49
+ status: 408,
50
+ billable: false,
51
+ when: 'The page did not finish within timeoutMs and was force-closed.',
52
+ whatToDo: 'Raise timeoutMs (max 120000), or relax waitFor — networkIdle on a page with a long-poll never settles.',
53
+ },
54
+ {
55
+ code: 'render_failed',
56
+ status: 422,
57
+ billable: true,
58
+ when: 'Chromium ran but the page threw, navigation failed, or waitFor.selector never appeared.',
59
+ whatToDo: 'Load the same HTML in a browser. This one is billable because the browser did the work.',
60
+ },
61
+ {
62
+ code: 'rate_limited',
63
+ status: 429,
64
+ billable: false,
65
+ when: 'More requests per second than the plan allows.',
66
+ whatToDo: 'Honour the Retry-After header and back off. Paid plans allow 20 req/s.',
67
+ },
68
+ {
69
+ code: 'quota_exceeded',
70
+ status: 429,
71
+ billable: false,
72
+ when: 'A free-plan account has used all 100 renders in the current period.',
73
+ whatToDo: 'Upgrade, or wait for resets_at from GET /v1/usage. Paid plans never hard-stop; they accrue overage.',
74
+ },
75
+ {
76
+ code: 'internal_error',
77
+ status: 500,
78
+ billable: false,
79
+ when: 'Something on our side broke.',
80
+ whatToDo: 'Retry with backoff. Never billed. If it persists, send us the render id.',
81
+ },
82
+ ];
83
+ export const ERROR_SPEC_BY_CODE = Object.fromEntries(ERROR_TABLE.map((spec) => [spec.code, spec]));
84
+ export function docsUrlFor(code) {
85
+ return `${DOCS_ORIGIN}/errors#${code}`;
86
+ }
87
+ //# 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;AAE3E,MAAM,CAAC,MAAM,WAAW,GAAG,sBAAsB,CAAC;AAElD,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,iBAAiB;IACjB,iBAAiB;IACjB,kBAAkB;IAClB,WAAW;IACX,gBAAgB;IAChB,eAAe;IACf,cAAc;IACd,gBAAgB;IAChB,gBAAgB;CACR,CAAC;AAaX,MAAM,CAAC,MAAM,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;AAE1C,MAAM,CAAC,MAAM,kBAAkB,GAA2C,MAAM,CAAC,WAAW,CAC1F,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CACb,CAAC;AAElC,MAAM,UAAU,UAAU,CAAC,IAAe;IACxC,OAAO,GAAG,WAAW,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,3 @@
1
+ export * from './render-options.js';
2
+ export * from './request.js';
3
+ export * from './errors.js';
@@ -0,0 +1,7 @@
1
+ // GENERATED FILE — do not edit.
2
+ // The same option, plan and error definitions the PDFCraft API validates
3
+ // requests against. Regenerated by packages/contract/scripts/sync-sdk.mjs.
4
+ export * from './render-options.js';
5
+ export * from './request.js';
6
+ export * from './errors.js';
7
+ //# 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,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC","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"]}