@formfeed/sdk 0.1.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 +63 -0
- package/index.cjs +267 -0
- package/index.js +244 -0
- package/package.json +61 -0
- package/types/index.d.cts +4 -0
- package/types/index.d.ts +4 -0
- package/types/lib/client.d.cts +295 -0
- package/types/lib/client.d.ts +295 -0
- package/types/lib/webhooks.d.cts +19 -0
- package/types/lib/webhooks.d.ts +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Philipp Staudt IT-Dienstleistungen
|
|
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,63 @@
|
|
|
1
|
+
# @formfeed/sdk
|
|
2
|
+
|
|
3
|
+
The TypeScript client for the [Formfeed](https://formfeed.dev) API: generate PDFs and images from
|
|
4
|
+
templates. It has no dependencies and uses only Web APIs (`fetch`, Web Crypto), so it runs in Node 22+,
|
|
5
|
+
Deno, Bun, Cloudflare Workers and browsers.
|
|
6
|
+
|
|
7
|
+
- Retries `429` and `503` with the server's `Retry-After`.
|
|
8
|
+
- Sends an `Idempotency-Key` with every render, so a retried request never renders twice.
|
|
9
|
+
- Turns API problems into `FormfeedError` with `code`, `status` and `request_id`.
|
|
10
|
+
- Verifies webhook signatures.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @formfeed/sdk
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Render a template
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { Formfeed } from '@formfeed/sdk';
|
|
20
|
+
|
|
21
|
+
const client = new Formfeed({ apiKey: process.env.FORMFEED_API_KEY!, region: 'eu' });
|
|
22
|
+
|
|
23
|
+
const render = await client.renders.create({
|
|
24
|
+
template: 'invoice-de',
|
|
25
|
+
data: { invoice: { number: '2026-0042', lines: [{ description: 'Consulting', qty: 8, price: 120 }] } },
|
|
26
|
+
});
|
|
27
|
+
console.log(render.download_url, render.page_count);
|
|
28
|
+
|
|
29
|
+
const pdf = await client.renders.download(render); // the bytes
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Async renders and batches
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const queued = await client.renders.create({ template: 'invoice-de', data, mode: 'async' });
|
|
36
|
+
const done = await client.renders.waitFor(queued.id);
|
|
37
|
+
|
|
38
|
+
const job = await client.renders.batch({
|
|
39
|
+
template: 'invoice-de',
|
|
40
|
+
items: orders.map((order) => ({ data: { invoice: order } })),
|
|
41
|
+
zip: true,
|
|
42
|
+
});
|
|
43
|
+
const finished = await client.jobs.waitFor(job.id);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Webhooks
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { parseWebhookEvent } from '@formfeed/sdk';
|
|
50
|
+
|
|
51
|
+
// pass the raw request body, not re-serialised JSON; throws when the signature does not match
|
|
52
|
+
const event = await parseWebhookEvent(secret, request.headers.get('webhook-signature'), rawBody);
|
|
53
|
+
if (event.type === 'render.completed') console.log(event.data); // the render object
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Documentation
|
|
57
|
+
|
|
58
|
+
- [TypeScript SDK guide](https://docs.formfeed.dev/api/sdks/typescript)
|
|
59
|
+
- [API reference](https://docs.formfeed.dev/api/overview)
|
|
60
|
+
|
|
61
|
+
## Licence
|
|
62
|
+
|
|
63
|
+
MIT
|
package/index.cjs
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// packages/sdk-ts/src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Formfeed: () => Formfeed,
|
|
24
|
+
FormfeedError: () => FormfeedError,
|
|
25
|
+
parseWebhookEvent: () => parseWebhookEvent,
|
|
26
|
+
verifyWebhookSignature: () => verifyWebhookSignature
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// packages/sdk-ts/src/lib/client.ts
|
|
31
|
+
var FormfeedError = class extends Error {
|
|
32
|
+
constructor(code, message, status, problem = null, requestId = null) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.code = code;
|
|
35
|
+
this.status = status;
|
|
36
|
+
this.problem = problem;
|
|
37
|
+
this.requestId = requestId;
|
|
38
|
+
}
|
|
39
|
+
name = "FormfeedError";
|
|
40
|
+
};
|
|
41
|
+
var hosts = {
|
|
42
|
+
eu: "https://api-eu.formfeed.dev/v1",
|
|
43
|
+
us: "https://api-us.formfeed.dev/v1"
|
|
44
|
+
};
|
|
45
|
+
var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
46
|
+
const t = setTimeout(resolve, ms);
|
|
47
|
+
signal?.addEventListener("abort", () => {
|
|
48
|
+
clearTimeout(t);
|
|
49
|
+
reject(signal.reason ?? new Error("aborted"));
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
function randomKey() {
|
|
53
|
+
const c = globalThis.crypto;
|
|
54
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
55
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
56
|
+
}
|
|
57
|
+
var Formfeed = class {
|
|
58
|
+
baseUrl;
|
|
59
|
+
apiKey;
|
|
60
|
+
fetchImpl;
|
|
61
|
+
maxRetries;
|
|
62
|
+
timeoutMs;
|
|
63
|
+
constructor(options) {
|
|
64
|
+
if (!options.apiKey) throw new FormfeedError("invalid_request", "apiKey is required", 0);
|
|
65
|
+
this.apiKey = options.apiKey;
|
|
66
|
+
this.baseUrl = (options.baseUrl ?? hosts[options.region ?? "eu"]).replace(/\/$/, "");
|
|
67
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
68
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
69
|
+
this.timeoutMs = options.timeoutMs ?? 12e4;
|
|
70
|
+
if (!this.fetchImpl) throw new FormfeedError("invalid_request", "fetch is not available; pass options.fetch", 0);
|
|
71
|
+
}
|
|
72
|
+
renders = {
|
|
73
|
+
/** Renders a template, HTML or URL. Sync by default; `mode: 'async'` returns a queued render. */
|
|
74
|
+
create: (request, options = {}) => this.request("POST", "/renders", request, {
|
|
75
|
+
...options,
|
|
76
|
+
idempotencyKey: options.idempotencyKey ?? randomKey()
|
|
77
|
+
}),
|
|
78
|
+
get: (id, options = {}) => this.request("GET", `/renders/${encodeURIComponent(id)}`, void 0, options),
|
|
79
|
+
/** Polls until the render succeeded or failed (async renders). */
|
|
80
|
+
waitFor: async (id, options = {}) => {
|
|
81
|
+
const deadline = Date.now() + (options.timeoutMs ?? 12e4);
|
|
82
|
+
const interval = options.intervalMs ?? 1e3;
|
|
83
|
+
for (; ; ) {
|
|
84
|
+
const render = await this.renders.get(id, { signal: options.signal });
|
|
85
|
+
if (render.status === "succeeded" || render.status === "failed") return render;
|
|
86
|
+
if (Date.now() + interval > deadline)
|
|
87
|
+
throw new FormfeedError("timeout", `render ${id} did not finish within the wait time`, 0);
|
|
88
|
+
await sleep(interval, options.signal);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
/** Downloads the output of a finished render as bytes. */
|
|
92
|
+
download: async (render, options = {}) => {
|
|
93
|
+
const target = typeof render === "string" ? await this.renders.get(render, options) : render;
|
|
94
|
+
if (!target.download_url)
|
|
95
|
+
throw new FormfeedError("not_ready", `render ${target.id} has no output (${target.status})`, 0);
|
|
96
|
+
const res = await this.fetchImpl(target.download_url, { signal: options.signal });
|
|
97
|
+
if (!res.ok) throw new FormfeedError("download_failed", `download answered HTTP ${res.status}`, res.status);
|
|
98
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
99
|
+
},
|
|
100
|
+
/** Page of renders, newest first. */
|
|
101
|
+
list: (query = {}, options = {}) => {
|
|
102
|
+
const params = new URLSearchParams();
|
|
103
|
+
for (const [k, v] of Object.entries(query)) if (v !== void 0 && v !== null && v !== "") params.set(k, String(v));
|
|
104
|
+
const qs = params.toString();
|
|
105
|
+
return this.request("GET", `/renders${qs ? `?${qs}` : ""}`, void 0, options);
|
|
106
|
+
},
|
|
107
|
+
/** Every render matching the query, following the cursor. */
|
|
108
|
+
all: async (query = {}, options = {}) => {
|
|
109
|
+
const out = [];
|
|
110
|
+
let cursor = null;
|
|
111
|
+
do {
|
|
112
|
+
const page = await this.renders.list({ ...query, cursor }, options);
|
|
113
|
+
out.push(...page.data);
|
|
114
|
+
cursor = page.next_cursor;
|
|
115
|
+
} while (cursor);
|
|
116
|
+
return out;
|
|
117
|
+
},
|
|
118
|
+
/** Removes the stored files of a render before they expire. Needs the `file:delete` scope. */
|
|
119
|
+
deleteOutputs: (id, options = {}) => this.request("DELETE", `/renders/${encodeURIComponent(id)}/outputs`, void 0, options),
|
|
120
|
+
batch: (request, options = {}) => this.request("POST", "/renders/batch", request, {
|
|
121
|
+
...options,
|
|
122
|
+
idempotencyKey: options.idempotencyKey ?? randomKey()
|
|
123
|
+
})
|
|
124
|
+
};
|
|
125
|
+
jobs = {
|
|
126
|
+
get: (id, options = {}) => this.request("GET", `/jobs/${encodeURIComponent(id)}`, void 0, options),
|
|
127
|
+
waitFor: async (id, options = {}) => {
|
|
128
|
+
const deadline = Date.now() + (options.timeoutMs ?? 6e5);
|
|
129
|
+
const interval = options.intervalMs ?? 2e3;
|
|
130
|
+
for (; ; ) {
|
|
131
|
+
const job = await this.jobs.get(id, { signal: options.signal });
|
|
132
|
+
if (job.status !== "queued" && job.status !== "processing") return job;
|
|
133
|
+
if (Date.now() + interval > deadline)
|
|
134
|
+
throw new FormfeedError("timeout", `job ${id} did not finish within the wait time`, 0);
|
|
135
|
+
await sleep(interval, options.signal);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
webhooks = {
|
|
140
|
+
list: async (options = {}) => (await this.request("GET", "/webhooks", void 0, options)).data,
|
|
141
|
+
create: (input, options = {}) => this.request("POST", "/webhooks", input, options),
|
|
142
|
+
update: (id, patch, options = {}) => this.request("PUT", `/webhooks/${encodeURIComponent(id)}`, patch, options),
|
|
143
|
+
delete: (id, options = {}) => this.request("DELETE", `/webhooks/${encodeURIComponent(id)}`, void 0, options),
|
|
144
|
+
test: (id, options = {}) => this.request("POST", `/webhooks/${encodeURIComponent(id)}/test`, void 0, options)
|
|
145
|
+
};
|
|
146
|
+
templates = {
|
|
147
|
+
list: (query = {}, options = {}) => {
|
|
148
|
+
const params = new URLSearchParams();
|
|
149
|
+
for (const [k, v] of Object.entries(query)) if (v !== void 0 && v !== null && v !== "") params.set(k, String(v));
|
|
150
|
+
const qs = params.toString();
|
|
151
|
+
return this.request("GET", `/templates${qs ? `?${qs}` : ""}`, void 0, options);
|
|
152
|
+
},
|
|
153
|
+
/** Every template of the workspace, following the cursor. */
|
|
154
|
+
all: async (query = {}, options = {}) => {
|
|
155
|
+
const out = [];
|
|
156
|
+
let cursor = null;
|
|
157
|
+
do {
|
|
158
|
+
const page = await this.templates.list({ ...query, cursor }, options);
|
|
159
|
+
out.push(...page.data);
|
|
160
|
+
cursor = page.next_cursor;
|
|
161
|
+
} while (cursor);
|
|
162
|
+
return out;
|
|
163
|
+
},
|
|
164
|
+
get: (idOrSlug, options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}`, void 0, options),
|
|
165
|
+
create: (input, options = {}) => this.request("POST", "/templates", input, options),
|
|
166
|
+
update: (idOrSlug, patch, options = {}) => this.request("PUT", `/templates/${encodeURIComponent(idOrSlug)}`, patch, options),
|
|
167
|
+
archive: (idOrSlug, options = {}) => this.request("DELETE", `/templates/${encodeURIComponent(idOrSlug)}`, void 0, options),
|
|
168
|
+
versions: {
|
|
169
|
+
list: async (idOrSlug, options = {}) => (await this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/versions`, void 0, options)).data,
|
|
170
|
+
/** `which`: 'published', 'latest' or a version number. Carries the files. */
|
|
171
|
+
get: (idOrSlug, which = "published", options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/versions/${which}`, void 0, options),
|
|
172
|
+
create: (idOrSlug, input, options = {}) => this.request("POST", `/templates/${encodeURIComponent(idOrSlug)}/versions`, input, options),
|
|
173
|
+
publish: (idOrSlug, number, options = {}) => this.request("POST", `/templates/${encodeURIComponent(idOrSlug)}/versions/${number}/publish`, void 0, options)
|
|
174
|
+
},
|
|
175
|
+
schema: (idOrSlug, options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/schema`, void 0, options)
|
|
176
|
+
};
|
|
177
|
+
account = {
|
|
178
|
+
get: (options = {}) => this.request("GET", "/account", void 0, options),
|
|
179
|
+
/** Units of the current period, or of `YYYY-MM`, with a daily series and a breakdown per template. */
|
|
180
|
+
usage: (period = "current", options = {}) => this.request("GET", `/usage?period=${encodeURIComponent(period)}`, void 0, options)
|
|
181
|
+
};
|
|
182
|
+
/** Raw request with auth, retries and problem mapping; use it for endpoints without a helper. */
|
|
183
|
+
async request(method, path, body, options = {}) {
|
|
184
|
+
const url = `${this.baseUrl}${path}`;
|
|
185
|
+
const headers = {
|
|
186
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
187
|
+
accept: "application/json",
|
|
188
|
+
"user-agent": "formfeed-sdk-ts/0.1"
|
|
189
|
+
};
|
|
190
|
+
if (body !== void 0) headers["content-type"] = "application/json";
|
|
191
|
+
if (options.idempotencyKey) headers["idempotency-key"] = options.idempotencyKey;
|
|
192
|
+
let attempt = 0;
|
|
193
|
+
for (; ; ) {
|
|
194
|
+
const controller = new AbortController();
|
|
195
|
+
const timer = setTimeout(() => controller.abort(new Error("request timed out")), this.timeoutMs);
|
|
196
|
+
options.signal?.addEventListener("abort", () => controller.abort(options.signal?.reason));
|
|
197
|
+
let res;
|
|
198
|
+
try {
|
|
199
|
+
res = await this.fetchImpl(url, {
|
|
200
|
+
method,
|
|
201
|
+
headers,
|
|
202
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
203
|
+
signal: controller.signal
|
|
204
|
+
});
|
|
205
|
+
} catch (e) {
|
|
206
|
+
clearTimeout(timer);
|
|
207
|
+
if (attempt < this.maxRetries && !options.signal?.aborted) {
|
|
208
|
+
attempt++;
|
|
209
|
+
await sleep(this.backoff(attempt, null), options.signal);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
throw new FormfeedError("network_error", e instanceof Error ? e.message : String(e), 0);
|
|
213
|
+
}
|
|
214
|
+
clearTimeout(timer);
|
|
215
|
+
if ((res.status === 429 || res.status === 503) && attempt < this.maxRetries) {
|
|
216
|
+
attempt++;
|
|
217
|
+
await sleep(this.backoff(attempt, res.headers.get("retry-after")), options.signal);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (res.status === 204) return void 0;
|
|
221
|
+
const text = await res.text();
|
|
222
|
+
const json = text ? JSON.parse(text) : null;
|
|
223
|
+
if (!res.ok) {
|
|
224
|
+
const problem = json ?? {};
|
|
225
|
+
throw new FormfeedError(
|
|
226
|
+
problem.code ?? `http_${res.status}`,
|
|
227
|
+
problem.detail ?? problem.title ?? `HTTP ${res.status}`,
|
|
228
|
+
res.status,
|
|
229
|
+
problem,
|
|
230
|
+
res.headers.get("x-request-id")
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return json;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
backoff(attempt, retryAfter) {
|
|
237
|
+
const fromHeader = retryAfter ? Number(retryAfter) * 1e3 : NaN;
|
|
238
|
+
const base = Number.isFinite(fromHeader) && fromHeader > 0 ? fromHeader : 500 * 2 ** (attempt - 1);
|
|
239
|
+
return Math.min(3e4, base + Math.random() * 250);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// packages/sdk-ts/src/lib/webhooks.ts
|
|
244
|
+
var encoder = new TextEncoder();
|
|
245
|
+
async function hmacHex(secret, message) {
|
|
246
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
247
|
+
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(message));
|
|
248
|
+
return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
249
|
+
}
|
|
250
|
+
async function verifyWebhookSignature(secret, signatureHeader, rawBody, options = {}) {
|
|
251
|
+
if (!signatureHeader || !secret) return false;
|
|
252
|
+
const parts = new Map(signatureHeader.split(",").map((kv) => kv.trim().split("=")));
|
|
253
|
+
const t = Number(parts.get("t"));
|
|
254
|
+
const v1 = (parts.get("v1") ?? "").toLowerCase();
|
|
255
|
+
if (!Number.isFinite(t) || !/^[0-9a-f]{64}$/.test(v1)) return false;
|
|
256
|
+
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
257
|
+
if (Math.abs(now - t) > (options.toleranceSeconds ?? 300)) return false;
|
|
258
|
+
const expected = await hmacHex(secret, `${t}.${rawBody}`);
|
|
259
|
+
let diff = 0;
|
|
260
|
+
for (let i = 0; i < expected.length; i++) diff |= expected.charCodeAt(i) ^ v1.charCodeAt(i);
|
|
261
|
+
return diff === 0;
|
|
262
|
+
}
|
|
263
|
+
async function parseWebhookEvent(secret, signatureHeader, rawBody, options = {}) {
|
|
264
|
+
if (!await verifyWebhookSignature(secret, signatureHeader, rawBody, options))
|
|
265
|
+
throw new Error("invalid webhook signature");
|
|
266
|
+
return JSON.parse(rawBody);
|
|
267
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// packages/sdk-ts/src/lib/client.ts
|
|
2
|
+
var FormfeedError = class extends Error {
|
|
3
|
+
constructor(code, message, status, problem = null, requestId = null) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.problem = problem;
|
|
8
|
+
this.requestId = requestId;
|
|
9
|
+
}
|
|
10
|
+
name = "FormfeedError";
|
|
11
|
+
};
|
|
12
|
+
var hosts = {
|
|
13
|
+
eu: "https://api-eu.formfeed.dev/v1",
|
|
14
|
+
us: "https://api-us.formfeed.dev/v1"
|
|
15
|
+
};
|
|
16
|
+
var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
17
|
+
const t = setTimeout(resolve, ms);
|
|
18
|
+
signal?.addEventListener("abort", () => {
|
|
19
|
+
clearTimeout(t);
|
|
20
|
+
reject(signal.reason ?? new Error("aborted"));
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
function randomKey() {
|
|
24
|
+
const c = globalThis.crypto;
|
|
25
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
26
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
27
|
+
}
|
|
28
|
+
var Formfeed = class {
|
|
29
|
+
baseUrl;
|
|
30
|
+
apiKey;
|
|
31
|
+
fetchImpl;
|
|
32
|
+
maxRetries;
|
|
33
|
+
timeoutMs;
|
|
34
|
+
constructor(options) {
|
|
35
|
+
if (!options.apiKey) throw new FormfeedError("invalid_request", "apiKey is required", 0);
|
|
36
|
+
this.apiKey = options.apiKey;
|
|
37
|
+
this.baseUrl = (options.baseUrl ?? hosts[options.region ?? "eu"]).replace(/\/$/, "");
|
|
38
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
39
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
40
|
+
this.timeoutMs = options.timeoutMs ?? 12e4;
|
|
41
|
+
if (!this.fetchImpl) throw new FormfeedError("invalid_request", "fetch is not available; pass options.fetch", 0);
|
|
42
|
+
}
|
|
43
|
+
renders = {
|
|
44
|
+
/** Renders a template, HTML or URL. Sync by default; `mode: 'async'` returns a queued render. */
|
|
45
|
+
create: (request, options = {}) => this.request("POST", "/renders", request, {
|
|
46
|
+
...options,
|
|
47
|
+
idempotencyKey: options.idempotencyKey ?? randomKey()
|
|
48
|
+
}),
|
|
49
|
+
get: (id, options = {}) => this.request("GET", `/renders/${encodeURIComponent(id)}`, void 0, options),
|
|
50
|
+
/** Polls until the render succeeded or failed (async renders). */
|
|
51
|
+
waitFor: async (id, options = {}) => {
|
|
52
|
+
const deadline = Date.now() + (options.timeoutMs ?? 12e4);
|
|
53
|
+
const interval = options.intervalMs ?? 1e3;
|
|
54
|
+
for (; ; ) {
|
|
55
|
+
const render = await this.renders.get(id, { signal: options.signal });
|
|
56
|
+
if (render.status === "succeeded" || render.status === "failed") return render;
|
|
57
|
+
if (Date.now() + interval > deadline)
|
|
58
|
+
throw new FormfeedError("timeout", `render ${id} did not finish within the wait time`, 0);
|
|
59
|
+
await sleep(interval, options.signal);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
/** Downloads the output of a finished render as bytes. */
|
|
63
|
+
download: async (render, options = {}) => {
|
|
64
|
+
const target = typeof render === "string" ? await this.renders.get(render, options) : render;
|
|
65
|
+
if (!target.download_url)
|
|
66
|
+
throw new FormfeedError("not_ready", `render ${target.id} has no output (${target.status})`, 0);
|
|
67
|
+
const res = await this.fetchImpl(target.download_url, { signal: options.signal });
|
|
68
|
+
if (!res.ok) throw new FormfeedError("download_failed", `download answered HTTP ${res.status}`, res.status);
|
|
69
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
70
|
+
},
|
|
71
|
+
/** Page of renders, newest first. */
|
|
72
|
+
list: (query = {}, options = {}) => {
|
|
73
|
+
const params = new URLSearchParams();
|
|
74
|
+
for (const [k, v] of Object.entries(query)) if (v !== void 0 && v !== null && v !== "") params.set(k, String(v));
|
|
75
|
+
const qs = params.toString();
|
|
76
|
+
return this.request("GET", `/renders${qs ? `?${qs}` : ""}`, void 0, options);
|
|
77
|
+
},
|
|
78
|
+
/** Every render matching the query, following the cursor. */
|
|
79
|
+
all: async (query = {}, options = {}) => {
|
|
80
|
+
const out = [];
|
|
81
|
+
let cursor = null;
|
|
82
|
+
do {
|
|
83
|
+
const page = await this.renders.list({ ...query, cursor }, options);
|
|
84
|
+
out.push(...page.data);
|
|
85
|
+
cursor = page.next_cursor;
|
|
86
|
+
} while (cursor);
|
|
87
|
+
return out;
|
|
88
|
+
},
|
|
89
|
+
/** Removes the stored files of a render before they expire. Needs the `file:delete` scope. */
|
|
90
|
+
deleteOutputs: (id, options = {}) => this.request("DELETE", `/renders/${encodeURIComponent(id)}/outputs`, void 0, options),
|
|
91
|
+
batch: (request, options = {}) => this.request("POST", "/renders/batch", request, {
|
|
92
|
+
...options,
|
|
93
|
+
idempotencyKey: options.idempotencyKey ?? randomKey()
|
|
94
|
+
})
|
|
95
|
+
};
|
|
96
|
+
jobs = {
|
|
97
|
+
get: (id, options = {}) => this.request("GET", `/jobs/${encodeURIComponent(id)}`, void 0, options),
|
|
98
|
+
waitFor: async (id, options = {}) => {
|
|
99
|
+
const deadline = Date.now() + (options.timeoutMs ?? 6e5);
|
|
100
|
+
const interval = options.intervalMs ?? 2e3;
|
|
101
|
+
for (; ; ) {
|
|
102
|
+
const job = await this.jobs.get(id, { signal: options.signal });
|
|
103
|
+
if (job.status !== "queued" && job.status !== "processing") return job;
|
|
104
|
+
if (Date.now() + interval > deadline)
|
|
105
|
+
throw new FormfeedError("timeout", `job ${id} did not finish within the wait time`, 0);
|
|
106
|
+
await sleep(interval, options.signal);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
webhooks = {
|
|
111
|
+
list: async (options = {}) => (await this.request("GET", "/webhooks", void 0, options)).data,
|
|
112
|
+
create: (input, options = {}) => this.request("POST", "/webhooks", input, options),
|
|
113
|
+
update: (id, patch, options = {}) => this.request("PUT", `/webhooks/${encodeURIComponent(id)}`, patch, options),
|
|
114
|
+
delete: (id, options = {}) => this.request("DELETE", `/webhooks/${encodeURIComponent(id)}`, void 0, options),
|
|
115
|
+
test: (id, options = {}) => this.request("POST", `/webhooks/${encodeURIComponent(id)}/test`, void 0, options)
|
|
116
|
+
};
|
|
117
|
+
templates = {
|
|
118
|
+
list: (query = {}, options = {}) => {
|
|
119
|
+
const params = new URLSearchParams();
|
|
120
|
+
for (const [k, v] of Object.entries(query)) if (v !== void 0 && v !== null && v !== "") params.set(k, String(v));
|
|
121
|
+
const qs = params.toString();
|
|
122
|
+
return this.request("GET", `/templates${qs ? `?${qs}` : ""}`, void 0, options);
|
|
123
|
+
},
|
|
124
|
+
/** Every template of the workspace, following the cursor. */
|
|
125
|
+
all: async (query = {}, options = {}) => {
|
|
126
|
+
const out = [];
|
|
127
|
+
let cursor = null;
|
|
128
|
+
do {
|
|
129
|
+
const page = await this.templates.list({ ...query, cursor }, options);
|
|
130
|
+
out.push(...page.data);
|
|
131
|
+
cursor = page.next_cursor;
|
|
132
|
+
} while (cursor);
|
|
133
|
+
return out;
|
|
134
|
+
},
|
|
135
|
+
get: (idOrSlug, options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}`, void 0, options),
|
|
136
|
+
create: (input, options = {}) => this.request("POST", "/templates", input, options),
|
|
137
|
+
update: (idOrSlug, patch, options = {}) => this.request("PUT", `/templates/${encodeURIComponent(idOrSlug)}`, patch, options),
|
|
138
|
+
archive: (idOrSlug, options = {}) => this.request("DELETE", `/templates/${encodeURIComponent(idOrSlug)}`, void 0, options),
|
|
139
|
+
versions: {
|
|
140
|
+
list: async (idOrSlug, options = {}) => (await this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/versions`, void 0, options)).data,
|
|
141
|
+
/** `which`: 'published', 'latest' or a version number. Carries the files. */
|
|
142
|
+
get: (idOrSlug, which = "published", options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/versions/${which}`, void 0, options),
|
|
143
|
+
create: (idOrSlug, input, options = {}) => this.request("POST", `/templates/${encodeURIComponent(idOrSlug)}/versions`, input, options),
|
|
144
|
+
publish: (idOrSlug, number, options = {}) => this.request("POST", `/templates/${encodeURIComponent(idOrSlug)}/versions/${number}/publish`, void 0, options)
|
|
145
|
+
},
|
|
146
|
+
schema: (idOrSlug, options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/schema`, void 0, options)
|
|
147
|
+
};
|
|
148
|
+
account = {
|
|
149
|
+
get: (options = {}) => this.request("GET", "/account", void 0, options),
|
|
150
|
+
/** Units of the current period, or of `YYYY-MM`, with a daily series and a breakdown per template. */
|
|
151
|
+
usage: (period = "current", options = {}) => this.request("GET", `/usage?period=${encodeURIComponent(period)}`, void 0, options)
|
|
152
|
+
};
|
|
153
|
+
/** Raw request with auth, retries and problem mapping; use it for endpoints without a helper. */
|
|
154
|
+
async request(method, path, body, options = {}) {
|
|
155
|
+
const url = `${this.baseUrl}${path}`;
|
|
156
|
+
const headers = {
|
|
157
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
158
|
+
accept: "application/json",
|
|
159
|
+
"user-agent": "formfeed-sdk-ts/0.1"
|
|
160
|
+
};
|
|
161
|
+
if (body !== void 0) headers["content-type"] = "application/json";
|
|
162
|
+
if (options.idempotencyKey) headers["idempotency-key"] = options.idempotencyKey;
|
|
163
|
+
let attempt = 0;
|
|
164
|
+
for (; ; ) {
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
const timer = setTimeout(() => controller.abort(new Error("request timed out")), this.timeoutMs);
|
|
167
|
+
options.signal?.addEventListener("abort", () => controller.abort(options.signal?.reason));
|
|
168
|
+
let res;
|
|
169
|
+
try {
|
|
170
|
+
res = await this.fetchImpl(url, {
|
|
171
|
+
method,
|
|
172
|
+
headers,
|
|
173
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
174
|
+
signal: controller.signal
|
|
175
|
+
});
|
|
176
|
+
} catch (e) {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
if (attempt < this.maxRetries && !options.signal?.aborted) {
|
|
179
|
+
attempt++;
|
|
180
|
+
await sleep(this.backoff(attempt, null), options.signal);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
throw new FormfeedError("network_error", e instanceof Error ? e.message : String(e), 0);
|
|
184
|
+
}
|
|
185
|
+
clearTimeout(timer);
|
|
186
|
+
if ((res.status === 429 || res.status === 503) && attempt < this.maxRetries) {
|
|
187
|
+
attempt++;
|
|
188
|
+
await sleep(this.backoff(attempt, res.headers.get("retry-after")), options.signal);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (res.status === 204) return void 0;
|
|
192
|
+
const text = await res.text();
|
|
193
|
+
const json = text ? JSON.parse(text) : null;
|
|
194
|
+
if (!res.ok) {
|
|
195
|
+
const problem = json ?? {};
|
|
196
|
+
throw new FormfeedError(
|
|
197
|
+
problem.code ?? `http_${res.status}`,
|
|
198
|
+
problem.detail ?? problem.title ?? `HTTP ${res.status}`,
|
|
199
|
+
res.status,
|
|
200
|
+
problem,
|
|
201
|
+
res.headers.get("x-request-id")
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return json;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
backoff(attempt, retryAfter) {
|
|
208
|
+
const fromHeader = retryAfter ? Number(retryAfter) * 1e3 : NaN;
|
|
209
|
+
const base = Number.isFinite(fromHeader) && fromHeader > 0 ? fromHeader : 500 * 2 ** (attempt - 1);
|
|
210
|
+
return Math.min(3e4, base + Math.random() * 250);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// packages/sdk-ts/src/lib/webhooks.ts
|
|
215
|
+
var encoder = new TextEncoder();
|
|
216
|
+
async function hmacHex(secret, message) {
|
|
217
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
218
|
+
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(message));
|
|
219
|
+
return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
220
|
+
}
|
|
221
|
+
async function verifyWebhookSignature(secret, signatureHeader, rawBody, options = {}) {
|
|
222
|
+
if (!signatureHeader || !secret) return false;
|
|
223
|
+
const parts = new Map(signatureHeader.split(",").map((kv) => kv.trim().split("=")));
|
|
224
|
+
const t = Number(parts.get("t"));
|
|
225
|
+
const v1 = (parts.get("v1") ?? "").toLowerCase();
|
|
226
|
+
if (!Number.isFinite(t) || !/^[0-9a-f]{64}$/.test(v1)) return false;
|
|
227
|
+
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
228
|
+
if (Math.abs(now - t) > (options.toleranceSeconds ?? 300)) return false;
|
|
229
|
+
const expected = await hmacHex(secret, `${t}.${rawBody}`);
|
|
230
|
+
let diff = 0;
|
|
231
|
+
for (let i = 0; i < expected.length; i++) diff |= expected.charCodeAt(i) ^ v1.charCodeAt(i);
|
|
232
|
+
return diff === 0;
|
|
233
|
+
}
|
|
234
|
+
async function parseWebhookEvent(secret, signatureHeader, rawBody, options = {}) {
|
|
235
|
+
if (!await verifyWebhookSignature(secret, signatureHeader, rawBody, options))
|
|
236
|
+
throw new Error("invalid webhook signature");
|
|
237
|
+
return JSON.parse(rawBody);
|
|
238
|
+
}
|
|
239
|
+
export {
|
|
240
|
+
Formfeed,
|
|
241
|
+
FormfeedError,
|
|
242
|
+
parseWebhookEvent,
|
|
243
|
+
verifyWebhookSignature
|
|
244
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@formfeed/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Formfeed API client: renders, templates, webhooks. No dependencies, Web APIs only.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./index.cjs",
|
|
8
|
+
"module": "./index.js",
|
|
9
|
+
"types": "./types/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./types/index.d.ts",
|
|
14
|
+
"default": "./index.js"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./types/index.d.cts",
|
|
18
|
+
"default": "./index.cjs"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"*.js",
|
|
25
|
+
"*.cjs",
|
|
26
|
+
"types",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"THIRD_PARTY_LICENSES.md"
|
|
30
|
+
],
|
|
31
|
+
"sideEffects": false,
|
|
32
|
+
"dependencies": {},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"formfeed",
|
|
35
|
+
"pdf",
|
|
36
|
+
"pdf-generation",
|
|
37
|
+
"html-to-pdf",
|
|
38
|
+
"image-generation",
|
|
39
|
+
"templates",
|
|
40
|
+
"api",
|
|
41
|
+
"sdk"
|
|
42
|
+
],
|
|
43
|
+
"homepage": "https://docs.formfeed.dev/api/sdks/typescript",
|
|
44
|
+
"author": {
|
|
45
|
+
"name": "Formfeed",
|
|
46
|
+
"email": "support@formfeed.dev",
|
|
47
|
+
"url": "https://formfeed.dev"
|
|
48
|
+
},
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/formfeed-dev/formfeed.git",
|
|
52
|
+
"directory": "packages/sdk-ts"
|
|
53
|
+
},
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/formfeed-dev/formfeed/issues",
|
|
56
|
+
"email": "support@formfeed.dev"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=22"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { Formfeed, FormfeedError } from './lib/client.cjs';
|
|
2
|
+
export type { Template, TemplateCreate, TemplateFiles, TemplateKind, TemplateListOptions, TemplateVersion, TemplateVersionCreate, VersionStatus, BatchRequest, Engine, FormfeedOptions, Job, OutputFormat, Problem, Region, Render, RenderListOptions, RenderRequest, RenderStatus, RequestOptions, Usage, WaitOptions, WebhookEndpoint, } from './lib/client.cjs';
|
|
3
|
+
export { parseWebhookEvent, verifyWebhookSignature } from './lib/webhooks.cjs';
|
|
4
|
+
export type { VerifyOptions } from './lib/webhooks.cjs';
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { Formfeed, FormfeedError } from './lib/client.js';
|
|
2
|
+
export type { Template, TemplateCreate, TemplateFiles, TemplateKind, TemplateListOptions, TemplateVersion, TemplateVersionCreate, VersionStatus, BatchRequest, Engine, FormfeedOptions, Job, OutputFormat, Problem, Region, Render, RenderListOptions, RenderRequest, RenderStatus, RequestOptions, Usage, WaitOptions, WebhookEndpoint, } from './lib/client.js';
|
|
3
|
+
export { parseWebhookEvent, verifyWebhookSignature } from './lib/webhooks.js';
|
|
4
|
+
export type { VerifyOptions } from './lib/webhooks.js';
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@formfeed/sdk` (spec 10 §1): a fetch-based client for the public API. Runs in Node 18+, Deno,
|
|
3
|
+
* Bun, Workers and browsers (with a test key). Retries 429 and 503 with the server's Retry-After
|
|
4
|
+
* and jitter, generates an Idempotency-Key for every render, and turns RFC 9457 problems into
|
|
5
|
+
* typed errors.
|
|
6
|
+
*/
|
|
7
|
+
export type Region = 'eu' | 'us';
|
|
8
|
+
export type Engine = 'jinja2' | 'liquid' | 'handlebars';
|
|
9
|
+
export type OutputFormat = 'pdf' | 'png' | 'jpg' | 'webp';
|
|
10
|
+
export type RenderStatus = 'queued' | 'rendering' | 'succeeded' | 'failed';
|
|
11
|
+
export interface FormfeedOptions {
|
|
12
|
+
apiKey: string;
|
|
13
|
+
/** Region of the API host; ignored when `baseUrl` is given. */
|
|
14
|
+
region?: Region;
|
|
15
|
+
/** Full base URL including `/v1`, for self-hosted or local gateways. */
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
fetch?: typeof fetch;
|
|
18
|
+
/** Retries on 429 and 503; 0 disables. */
|
|
19
|
+
maxRetries?: number;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface RenderRequest {
|
|
23
|
+
template?: string;
|
|
24
|
+
version?: string;
|
|
25
|
+
html?: string;
|
|
26
|
+
url?: string;
|
|
27
|
+
engine?: Engine;
|
|
28
|
+
data?: Record<string, unknown>;
|
|
29
|
+
output?: OutputFormat;
|
|
30
|
+
settings?: Record<string, unknown>;
|
|
31
|
+
filename?: string;
|
|
32
|
+
expires_in?: number | null;
|
|
33
|
+
access?: 'public' | 'signed';
|
|
34
|
+
mode?: 'sync' | 'async';
|
|
35
|
+
locale?: string | null;
|
|
36
|
+
webhook_url?: string | null;
|
|
37
|
+
meta?: Record<string, unknown>;
|
|
38
|
+
region?: Region;
|
|
39
|
+
dedupe?: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface Render {
|
|
42
|
+
id: string;
|
|
43
|
+
status: RenderStatus;
|
|
44
|
+
output: string;
|
|
45
|
+
download_url: string | null;
|
|
46
|
+
expires_at: string | null;
|
|
47
|
+
bytes: number | null;
|
|
48
|
+
page_count: number | null;
|
|
49
|
+
units: number;
|
|
50
|
+
region: string;
|
|
51
|
+
environment: 'live' | 'test';
|
|
52
|
+
template: {
|
|
53
|
+
id: string;
|
|
54
|
+
slug: string;
|
|
55
|
+
version: number;
|
|
56
|
+
} | null;
|
|
57
|
+
engine_version: string | null;
|
|
58
|
+
template_checksum: string | null;
|
|
59
|
+
output_sha256: string | null;
|
|
60
|
+
deduplicated: boolean;
|
|
61
|
+
timings: Record<string, number> | null;
|
|
62
|
+
error: {
|
|
63
|
+
code?: string;
|
|
64
|
+
message?: string;
|
|
65
|
+
[key: string]: unknown;
|
|
66
|
+
} | null;
|
|
67
|
+
meta: Record<string, unknown>;
|
|
68
|
+
created_at: string;
|
|
69
|
+
completed_at: string | null;
|
|
70
|
+
}
|
|
71
|
+
export interface BatchRequest {
|
|
72
|
+
template?: string;
|
|
73
|
+
items: RenderRequest[];
|
|
74
|
+
zip?: boolean;
|
|
75
|
+
webhook_url?: string | null;
|
|
76
|
+
meta?: Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
export interface Job {
|
|
79
|
+
id: string;
|
|
80
|
+
type: 'single' | 'batch';
|
|
81
|
+
status: 'queued' | 'processing' | 'completed' | 'partially_failed' | 'failed';
|
|
82
|
+
total: number;
|
|
83
|
+
succeeded: number;
|
|
84
|
+
failed: number;
|
|
85
|
+
zip_url: string | null;
|
|
86
|
+
zip_expires_at: string | null;
|
|
87
|
+
items: Render[];
|
|
88
|
+
meta: Record<string, unknown>;
|
|
89
|
+
created_at: string;
|
|
90
|
+
completed_at: string | null;
|
|
91
|
+
}
|
|
92
|
+
export interface WebhookEndpoint {
|
|
93
|
+
id: string;
|
|
94
|
+
url: string;
|
|
95
|
+
events: string[];
|
|
96
|
+
enabled: boolean;
|
|
97
|
+
description: string | null;
|
|
98
|
+
consecutive_failures: number;
|
|
99
|
+
/** Only present in the response of `create`. */
|
|
100
|
+
secret?: string;
|
|
101
|
+
created_at: string;
|
|
102
|
+
updated_at: string;
|
|
103
|
+
}
|
|
104
|
+
export type TemplateKind = 'pdf' | 'image';
|
|
105
|
+
export type VersionStatus = 'draft' | 'published' | 'archived';
|
|
106
|
+
export interface Template {
|
|
107
|
+
id: string;
|
|
108
|
+
slug: string;
|
|
109
|
+
name: string;
|
|
110
|
+
description: string | null;
|
|
111
|
+
kind: TemplateKind;
|
|
112
|
+
engine: Engine;
|
|
113
|
+
tags: string[];
|
|
114
|
+
published_version: number | null;
|
|
115
|
+
latest_version: number;
|
|
116
|
+
created_at: string;
|
|
117
|
+
updated_at: string;
|
|
118
|
+
}
|
|
119
|
+
/** The files of a version as the editor and the CLI keep them. */
|
|
120
|
+
export interface TemplateFiles {
|
|
121
|
+
html: string;
|
|
122
|
+
css?: string;
|
|
123
|
+
head?: string;
|
|
124
|
+
settings?: Record<string, unknown>;
|
|
125
|
+
sample_data?: Record<string, unknown>;
|
|
126
|
+
/** Named sample data sets besides the default one, as the editor and the dev kit show them. */
|
|
127
|
+
data_sets?: Record<string, unknown> | null;
|
|
128
|
+
data_schema?: Record<string, unknown> | null;
|
|
129
|
+
i18n?: Record<string, unknown> | null;
|
|
130
|
+
}
|
|
131
|
+
export interface TemplateVersion extends Partial<TemplateFiles> {
|
|
132
|
+
id: string;
|
|
133
|
+
number: number;
|
|
134
|
+
status: VersionStatus;
|
|
135
|
+
checksum: string;
|
|
136
|
+
change_note: string | null;
|
|
137
|
+
created_at: string;
|
|
138
|
+
published_at: string | null;
|
|
139
|
+
}
|
|
140
|
+
export interface TemplateCreate extends TemplateFiles {
|
|
141
|
+
name: string;
|
|
142
|
+
slug: string;
|
|
143
|
+
description?: string | null;
|
|
144
|
+
kind: TemplateKind;
|
|
145
|
+
engine: Engine;
|
|
146
|
+
tags?: string[];
|
|
147
|
+
publish?: boolean;
|
|
148
|
+
}
|
|
149
|
+
export interface TemplateVersionCreate extends TemplateFiles {
|
|
150
|
+
change_note?: string;
|
|
151
|
+
/** Checksum of the version the draft was based on; the API answers 409 when the latest differs. */
|
|
152
|
+
base_checksum?: string;
|
|
153
|
+
publish?: boolean;
|
|
154
|
+
}
|
|
155
|
+
export interface Usage {
|
|
156
|
+
period: string;
|
|
157
|
+
included: number;
|
|
158
|
+
used: number;
|
|
159
|
+
overage_used: number;
|
|
160
|
+
overage_balance: number;
|
|
161
|
+
daily: Array<{
|
|
162
|
+
date: string;
|
|
163
|
+
units: number;
|
|
164
|
+
renders: number;
|
|
165
|
+
}>;
|
|
166
|
+
by_template: Array<{
|
|
167
|
+
template: string;
|
|
168
|
+
units: number;
|
|
169
|
+
}>;
|
|
170
|
+
}
|
|
171
|
+
export interface RenderListOptions {
|
|
172
|
+
template?: string;
|
|
173
|
+
status?: RenderStatus;
|
|
174
|
+
environment?: 'live' | 'test';
|
|
175
|
+
/** ISO timestamps bounding `created_at`. */
|
|
176
|
+
since?: string;
|
|
177
|
+
until?: string;
|
|
178
|
+
limit?: number;
|
|
179
|
+
cursor?: string | null;
|
|
180
|
+
}
|
|
181
|
+
export interface TemplateListOptions {
|
|
182
|
+
kind?: TemplateKind;
|
|
183
|
+
engine?: Engine;
|
|
184
|
+
tag?: string;
|
|
185
|
+
q?: string;
|
|
186
|
+
limit?: number;
|
|
187
|
+
cursor?: string | null;
|
|
188
|
+
}
|
|
189
|
+
export interface Problem {
|
|
190
|
+
type?: string;
|
|
191
|
+
title?: string;
|
|
192
|
+
status?: number;
|
|
193
|
+
detail?: string;
|
|
194
|
+
code?: string;
|
|
195
|
+
request_id?: string;
|
|
196
|
+
[key: string]: unknown;
|
|
197
|
+
}
|
|
198
|
+
export declare class FormfeedError extends Error {
|
|
199
|
+
readonly code: string;
|
|
200
|
+
readonly status: number;
|
|
201
|
+
readonly problem: Problem | null;
|
|
202
|
+
readonly requestId: string | null;
|
|
203
|
+
readonly name = "FormfeedError";
|
|
204
|
+
constructor(code: string, message: string, status: number, problem?: Problem | null, requestId?: string | null);
|
|
205
|
+
}
|
|
206
|
+
export interface RequestOptions {
|
|
207
|
+
idempotencyKey?: string;
|
|
208
|
+
signal?: AbortSignal;
|
|
209
|
+
}
|
|
210
|
+
export interface WaitOptions {
|
|
211
|
+
timeoutMs?: number;
|
|
212
|
+
intervalMs?: number;
|
|
213
|
+
signal?: AbortSignal;
|
|
214
|
+
}
|
|
215
|
+
export declare class Formfeed {
|
|
216
|
+
private readonly baseUrl;
|
|
217
|
+
private readonly apiKey;
|
|
218
|
+
private readonly fetchImpl;
|
|
219
|
+
private readonly maxRetries;
|
|
220
|
+
private readonly timeoutMs;
|
|
221
|
+
constructor(options: FormfeedOptions);
|
|
222
|
+
readonly renders: {
|
|
223
|
+
/** Renders a template, HTML or URL. Sync by default; `mode: 'async'` returns a queued render. */
|
|
224
|
+
create: (request: RenderRequest, options?: RequestOptions) => Promise<Render>;
|
|
225
|
+
get: (id: string, options?: RequestOptions) => Promise<Render>;
|
|
226
|
+
/** Polls until the render succeeded or failed (async renders). */
|
|
227
|
+
waitFor: (id: string, options?: WaitOptions) => Promise<Render>;
|
|
228
|
+
/** Downloads the output of a finished render as bytes. */
|
|
229
|
+
download: (render: Render | string, options?: RequestOptions) => Promise<Uint8Array>;
|
|
230
|
+
/** Page of renders, newest first. */
|
|
231
|
+
list: (query?: RenderListOptions, options?: RequestOptions) => Promise<{
|
|
232
|
+
data: Render[];
|
|
233
|
+
next_cursor: string | null;
|
|
234
|
+
}>;
|
|
235
|
+
/** Every render matching the query, following the cursor. */
|
|
236
|
+
all: (query?: Omit<RenderListOptions, "cursor">, options?: RequestOptions) => Promise<Render[]>;
|
|
237
|
+
/** Removes the stored files of a render before they expire. Needs the `file:delete` scope. */
|
|
238
|
+
deleteOutputs: (id: string, options?: RequestOptions) => Promise<void>;
|
|
239
|
+
batch: (request: BatchRequest, options?: RequestOptions) => Promise<Job>;
|
|
240
|
+
};
|
|
241
|
+
readonly jobs: {
|
|
242
|
+
get: (id: string, options?: RequestOptions) => Promise<Job>;
|
|
243
|
+
waitFor: (id: string, options?: WaitOptions) => Promise<Job>;
|
|
244
|
+
};
|
|
245
|
+
readonly webhooks: {
|
|
246
|
+
list: (options?: RequestOptions) => Promise<WebhookEndpoint[]>;
|
|
247
|
+
create: (input: {
|
|
248
|
+
url: string;
|
|
249
|
+
events?: string[];
|
|
250
|
+
description?: string | null;
|
|
251
|
+
}, options?: RequestOptions) => Promise<WebhookEndpoint>;
|
|
252
|
+
update: (id: string, patch: {
|
|
253
|
+
url?: string;
|
|
254
|
+
events?: string[];
|
|
255
|
+
enabled?: boolean;
|
|
256
|
+
description?: string | null;
|
|
257
|
+
}, options?: RequestOptions) => Promise<WebhookEndpoint>;
|
|
258
|
+
delete: (id: string, options?: RequestOptions) => Promise<void>;
|
|
259
|
+
test: (id: string, options?: RequestOptions) => Promise<{
|
|
260
|
+
queued: boolean;
|
|
261
|
+
}>;
|
|
262
|
+
};
|
|
263
|
+
readonly templates: {
|
|
264
|
+
list: (query?: TemplateListOptions, options?: RequestOptions) => Promise<{
|
|
265
|
+
data: Template[];
|
|
266
|
+
next_cursor: string | null;
|
|
267
|
+
}>;
|
|
268
|
+
/** Every template of the workspace, following the cursor. */
|
|
269
|
+
all: (query?: Omit<TemplateListOptions, "cursor">, options?: RequestOptions) => Promise<Template[]>;
|
|
270
|
+
get: (idOrSlug: string, options?: RequestOptions) => Promise<Template>;
|
|
271
|
+
create: (input: TemplateCreate, options?: RequestOptions) => Promise<Template>;
|
|
272
|
+
update: (idOrSlug: string, patch: {
|
|
273
|
+
name?: string;
|
|
274
|
+
description?: string | null;
|
|
275
|
+
tags?: string[];
|
|
276
|
+
}, options?: RequestOptions) => Promise<Template>;
|
|
277
|
+
archive: (idOrSlug: string, options?: RequestOptions) => Promise<void>;
|
|
278
|
+
versions: {
|
|
279
|
+
list: (idOrSlug: string, options?: RequestOptions) => Promise<TemplateVersion[]>;
|
|
280
|
+
/** `which`: 'published', 'latest' or a version number. Carries the files. */
|
|
281
|
+
get: (idOrSlug: string, which?: "published" | "latest" | number, options?: RequestOptions) => Promise<TemplateVersion>;
|
|
282
|
+
create: (idOrSlug: string, input: TemplateVersionCreate, options?: RequestOptions) => Promise<TemplateVersion>;
|
|
283
|
+
publish: (idOrSlug: string, number: number, options?: RequestOptions) => Promise<TemplateVersion>;
|
|
284
|
+
};
|
|
285
|
+
schema: (idOrSlug: string, options?: RequestOptions) => Promise<Record<string, unknown>>;
|
|
286
|
+
};
|
|
287
|
+
readonly account: {
|
|
288
|
+
get: (options?: RequestOptions) => Promise<Record<string, unknown>>;
|
|
289
|
+
/** Units of the current period, or of `YYYY-MM`, with a daily series and a breakdown per template. */
|
|
290
|
+
usage: (period?: string, options?: RequestOptions) => Promise<Usage>;
|
|
291
|
+
};
|
|
292
|
+
/** Raw request with auth, retries and problem mapping; use it for endpoints without a helper. */
|
|
293
|
+
request<T>(method: string, path: string, body?: unknown, options?: RequestOptions): Promise<T>;
|
|
294
|
+
private backoff;
|
|
295
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@formfeed/sdk` (spec 10 §1): a fetch-based client for the public API. Runs in Node 18+, Deno,
|
|
3
|
+
* Bun, Workers and browsers (with a test key). Retries 429 and 503 with the server's Retry-After
|
|
4
|
+
* and jitter, generates an Idempotency-Key for every render, and turns RFC 9457 problems into
|
|
5
|
+
* typed errors.
|
|
6
|
+
*/
|
|
7
|
+
export type Region = 'eu' | 'us';
|
|
8
|
+
export type Engine = 'jinja2' | 'liquid' | 'handlebars';
|
|
9
|
+
export type OutputFormat = 'pdf' | 'png' | 'jpg' | 'webp';
|
|
10
|
+
export type RenderStatus = 'queued' | 'rendering' | 'succeeded' | 'failed';
|
|
11
|
+
export interface FormfeedOptions {
|
|
12
|
+
apiKey: string;
|
|
13
|
+
/** Region of the API host; ignored when `baseUrl` is given. */
|
|
14
|
+
region?: Region;
|
|
15
|
+
/** Full base URL including `/v1`, for self-hosted or local gateways. */
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
fetch?: typeof fetch;
|
|
18
|
+
/** Retries on 429 and 503; 0 disables. */
|
|
19
|
+
maxRetries?: number;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface RenderRequest {
|
|
23
|
+
template?: string;
|
|
24
|
+
version?: string;
|
|
25
|
+
html?: string;
|
|
26
|
+
url?: string;
|
|
27
|
+
engine?: Engine;
|
|
28
|
+
data?: Record<string, unknown>;
|
|
29
|
+
output?: OutputFormat;
|
|
30
|
+
settings?: Record<string, unknown>;
|
|
31
|
+
filename?: string;
|
|
32
|
+
expires_in?: number | null;
|
|
33
|
+
access?: 'public' | 'signed';
|
|
34
|
+
mode?: 'sync' | 'async';
|
|
35
|
+
locale?: string | null;
|
|
36
|
+
webhook_url?: string | null;
|
|
37
|
+
meta?: Record<string, unknown>;
|
|
38
|
+
region?: Region;
|
|
39
|
+
dedupe?: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface Render {
|
|
42
|
+
id: string;
|
|
43
|
+
status: RenderStatus;
|
|
44
|
+
output: string;
|
|
45
|
+
download_url: string | null;
|
|
46
|
+
expires_at: string | null;
|
|
47
|
+
bytes: number | null;
|
|
48
|
+
page_count: number | null;
|
|
49
|
+
units: number;
|
|
50
|
+
region: string;
|
|
51
|
+
environment: 'live' | 'test';
|
|
52
|
+
template: {
|
|
53
|
+
id: string;
|
|
54
|
+
slug: string;
|
|
55
|
+
version: number;
|
|
56
|
+
} | null;
|
|
57
|
+
engine_version: string | null;
|
|
58
|
+
template_checksum: string | null;
|
|
59
|
+
output_sha256: string | null;
|
|
60
|
+
deduplicated: boolean;
|
|
61
|
+
timings: Record<string, number> | null;
|
|
62
|
+
error: {
|
|
63
|
+
code?: string;
|
|
64
|
+
message?: string;
|
|
65
|
+
[key: string]: unknown;
|
|
66
|
+
} | null;
|
|
67
|
+
meta: Record<string, unknown>;
|
|
68
|
+
created_at: string;
|
|
69
|
+
completed_at: string | null;
|
|
70
|
+
}
|
|
71
|
+
export interface BatchRequest {
|
|
72
|
+
template?: string;
|
|
73
|
+
items: RenderRequest[];
|
|
74
|
+
zip?: boolean;
|
|
75
|
+
webhook_url?: string | null;
|
|
76
|
+
meta?: Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
export interface Job {
|
|
79
|
+
id: string;
|
|
80
|
+
type: 'single' | 'batch';
|
|
81
|
+
status: 'queued' | 'processing' | 'completed' | 'partially_failed' | 'failed';
|
|
82
|
+
total: number;
|
|
83
|
+
succeeded: number;
|
|
84
|
+
failed: number;
|
|
85
|
+
zip_url: string | null;
|
|
86
|
+
zip_expires_at: string | null;
|
|
87
|
+
items: Render[];
|
|
88
|
+
meta: Record<string, unknown>;
|
|
89
|
+
created_at: string;
|
|
90
|
+
completed_at: string | null;
|
|
91
|
+
}
|
|
92
|
+
export interface WebhookEndpoint {
|
|
93
|
+
id: string;
|
|
94
|
+
url: string;
|
|
95
|
+
events: string[];
|
|
96
|
+
enabled: boolean;
|
|
97
|
+
description: string | null;
|
|
98
|
+
consecutive_failures: number;
|
|
99
|
+
/** Only present in the response of `create`. */
|
|
100
|
+
secret?: string;
|
|
101
|
+
created_at: string;
|
|
102
|
+
updated_at: string;
|
|
103
|
+
}
|
|
104
|
+
export type TemplateKind = 'pdf' | 'image';
|
|
105
|
+
export type VersionStatus = 'draft' | 'published' | 'archived';
|
|
106
|
+
export interface Template {
|
|
107
|
+
id: string;
|
|
108
|
+
slug: string;
|
|
109
|
+
name: string;
|
|
110
|
+
description: string | null;
|
|
111
|
+
kind: TemplateKind;
|
|
112
|
+
engine: Engine;
|
|
113
|
+
tags: string[];
|
|
114
|
+
published_version: number | null;
|
|
115
|
+
latest_version: number;
|
|
116
|
+
created_at: string;
|
|
117
|
+
updated_at: string;
|
|
118
|
+
}
|
|
119
|
+
/** The files of a version as the editor and the CLI keep them. */
|
|
120
|
+
export interface TemplateFiles {
|
|
121
|
+
html: string;
|
|
122
|
+
css?: string;
|
|
123
|
+
head?: string;
|
|
124
|
+
settings?: Record<string, unknown>;
|
|
125
|
+
sample_data?: Record<string, unknown>;
|
|
126
|
+
/** Named sample data sets besides the default one, as the editor and the dev kit show them. */
|
|
127
|
+
data_sets?: Record<string, unknown> | null;
|
|
128
|
+
data_schema?: Record<string, unknown> | null;
|
|
129
|
+
i18n?: Record<string, unknown> | null;
|
|
130
|
+
}
|
|
131
|
+
export interface TemplateVersion extends Partial<TemplateFiles> {
|
|
132
|
+
id: string;
|
|
133
|
+
number: number;
|
|
134
|
+
status: VersionStatus;
|
|
135
|
+
checksum: string;
|
|
136
|
+
change_note: string | null;
|
|
137
|
+
created_at: string;
|
|
138
|
+
published_at: string | null;
|
|
139
|
+
}
|
|
140
|
+
export interface TemplateCreate extends TemplateFiles {
|
|
141
|
+
name: string;
|
|
142
|
+
slug: string;
|
|
143
|
+
description?: string | null;
|
|
144
|
+
kind: TemplateKind;
|
|
145
|
+
engine: Engine;
|
|
146
|
+
tags?: string[];
|
|
147
|
+
publish?: boolean;
|
|
148
|
+
}
|
|
149
|
+
export interface TemplateVersionCreate extends TemplateFiles {
|
|
150
|
+
change_note?: string;
|
|
151
|
+
/** Checksum of the version the draft was based on; the API answers 409 when the latest differs. */
|
|
152
|
+
base_checksum?: string;
|
|
153
|
+
publish?: boolean;
|
|
154
|
+
}
|
|
155
|
+
export interface Usage {
|
|
156
|
+
period: string;
|
|
157
|
+
included: number;
|
|
158
|
+
used: number;
|
|
159
|
+
overage_used: number;
|
|
160
|
+
overage_balance: number;
|
|
161
|
+
daily: Array<{
|
|
162
|
+
date: string;
|
|
163
|
+
units: number;
|
|
164
|
+
renders: number;
|
|
165
|
+
}>;
|
|
166
|
+
by_template: Array<{
|
|
167
|
+
template: string;
|
|
168
|
+
units: number;
|
|
169
|
+
}>;
|
|
170
|
+
}
|
|
171
|
+
export interface RenderListOptions {
|
|
172
|
+
template?: string;
|
|
173
|
+
status?: RenderStatus;
|
|
174
|
+
environment?: 'live' | 'test';
|
|
175
|
+
/** ISO timestamps bounding `created_at`. */
|
|
176
|
+
since?: string;
|
|
177
|
+
until?: string;
|
|
178
|
+
limit?: number;
|
|
179
|
+
cursor?: string | null;
|
|
180
|
+
}
|
|
181
|
+
export interface TemplateListOptions {
|
|
182
|
+
kind?: TemplateKind;
|
|
183
|
+
engine?: Engine;
|
|
184
|
+
tag?: string;
|
|
185
|
+
q?: string;
|
|
186
|
+
limit?: number;
|
|
187
|
+
cursor?: string | null;
|
|
188
|
+
}
|
|
189
|
+
export interface Problem {
|
|
190
|
+
type?: string;
|
|
191
|
+
title?: string;
|
|
192
|
+
status?: number;
|
|
193
|
+
detail?: string;
|
|
194
|
+
code?: string;
|
|
195
|
+
request_id?: string;
|
|
196
|
+
[key: string]: unknown;
|
|
197
|
+
}
|
|
198
|
+
export declare class FormfeedError extends Error {
|
|
199
|
+
readonly code: string;
|
|
200
|
+
readonly status: number;
|
|
201
|
+
readonly problem: Problem | null;
|
|
202
|
+
readonly requestId: string | null;
|
|
203
|
+
readonly name = "FormfeedError";
|
|
204
|
+
constructor(code: string, message: string, status: number, problem?: Problem | null, requestId?: string | null);
|
|
205
|
+
}
|
|
206
|
+
export interface RequestOptions {
|
|
207
|
+
idempotencyKey?: string;
|
|
208
|
+
signal?: AbortSignal;
|
|
209
|
+
}
|
|
210
|
+
export interface WaitOptions {
|
|
211
|
+
timeoutMs?: number;
|
|
212
|
+
intervalMs?: number;
|
|
213
|
+
signal?: AbortSignal;
|
|
214
|
+
}
|
|
215
|
+
export declare class Formfeed {
|
|
216
|
+
private readonly baseUrl;
|
|
217
|
+
private readonly apiKey;
|
|
218
|
+
private readonly fetchImpl;
|
|
219
|
+
private readonly maxRetries;
|
|
220
|
+
private readonly timeoutMs;
|
|
221
|
+
constructor(options: FormfeedOptions);
|
|
222
|
+
readonly renders: {
|
|
223
|
+
/** Renders a template, HTML or URL. Sync by default; `mode: 'async'` returns a queued render. */
|
|
224
|
+
create: (request: RenderRequest, options?: RequestOptions) => Promise<Render>;
|
|
225
|
+
get: (id: string, options?: RequestOptions) => Promise<Render>;
|
|
226
|
+
/** Polls until the render succeeded or failed (async renders). */
|
|
227
|
+
waitFor: (id: string, options?: WaitOptions) => Promise<Render>;
|
|
228
|
+
/** Downloads the output of a finished render as bytes. */
|
|
229
|
+
download: (render: Render | string, options?: RequestOptions) => Promise<Uint8Array>;
|
|
230
|
+
/** Page of renders, newest first. */
|
|
231
|
+
list: (query?: RenderListOptions, options?: RequestOptions) => Promise<{
|
|
232
|
+
data: Render[];
|
|
233
|
+
next_cursor: string | null;
|
|
234
|
+
}>;
|
|
235
|
+
/** Every render matching the query, following the cursor. */
|
|
236
|
+
all: (query?: Omit<RenderListOptions, "cursor">, options?: RequestOptions) => Promise<Render[]>;
|
|
237
|
+
/** Removes the stored files of a render before they expire. Needs the `file:delete` scope. */
|
|
238
|
+
deleteOutputs: (id: string, options?: RequestOptions) => Promise<void>;
|
|
239
|
+
batch: (request: BatchRequest, options?: RequestOptions) => Promise<Job>;
|
|
240
|
+
};
|
|
241
|
+
readonly jobs: {
|
|
242
|
+
get: (id: string, options?: RequestOptions) => Promise<Job>;
|
|
243
|
+
waitFor: (id: string, options?: WaitOptions) => Promise<Job>;
|
|
244
|
+
};
|
|
245
|
+
readonly webhooks: {
|
|
246
|
+
list: (options?: RequestOptions) => Promise<WebhookEndpoint[]>;
|
|
247
|
+
create: (input: {
|
|
248
|
+
url: string;
|
|
249
|
+
events?: string[];
|
|
250
|
+
description?: string | null;
|
|
251
|
+
}, options?: RequestOptions) => Promise<WebhookEndpoint>;
|
|
252
|
+
update: (id: string, patch: {
|
|
253
|
+
url?: string;
|
|
254
|
+
events?: string[];
|
|
255
|
+
enabled?: boolean;
|
|
256
|
+
description?: string | null;
|
|
257
|
+
}, options?: RequestOptions) => Promise<WebhookEndpoint>;
|
|
258
|
+
delete: (id: string, options?: RequestOptions) => Promise<void>;
|
|
259
|
+
test: (id: string, options?: RequestOptions) => Promise<{
|
|
260
|
+
queued: boolean;
|
|
261
|
+
}>;
|
|
262
|
+
};
|
|
263
|
+
readonly templates: {
|
|
264
|
+
list: (query?: TemplateListOptions, options?: RequestOptions) => Promise<{
|
|
265
|
+
data: Template[];
|
|
266
|
+
next_cursor: string | null;
|
|
267
|
+
}>;
|
|
268
|
+
/** Every template of the workspace, following the cursor. */
|
|
269
|
+
all: (query?: Omit<TemplateListOptions, "cursor">, options?: RequestOptions) => Promise<Template[]>;
|
|
270
|
+
get: (idOrSlug: string, options?: RequestOptions) => Promise<Template>;
|
|
271
|
+
create: (input: TemplateCreate, options?: RequestOptions) => Promise<Template>;
|
|
272
|
+
update: (idOrSlug: string, patch: {
|
|
273
|
+
name?: string;
|
|
274
|
+
description?: string | null;
|
|
275
|
+
tags?: string[];
|
|
276
|
+
}, options?: RequestOptions) => Promise<Template>;
|
|
277
|
+
archive: (idOrSlug: string, options?: RequestOptions) => Promise<void>;
|
|
278
|
+
versions: {
|
|
279
|
+
list: (idOrSlug: string, options?: RequestOptions) => Promise<TemplateVersion[]>;
|
|
280
|
+
/** `which`: 'published', 'latest' or a version number. Carries the files. */
|
|
281
|
+
get: (idOrSlug: string, which?: "published" | "latest" | number, options?: RequestOptions) => Promise<TemplateVersion>;
|
|
282
|
+
create: (idOrSlug: string, input: TemplateVersionCreate, options?: RequestOptions) => Promise<TemplateVersion>;
|
|
283
|
+
publish: (idOrSlug: string, number: number, options?: RequestOptions) => Promise<TemplateVersion>;
|
|
284
|
+
};
|
|
285
|
+
schema: (idOrSlug: string, options?: RequestOptions) => Promise<Record<string, unknown>>;
|
|
286
|
+
};
|
|
287
|
+
readonly account: {
|
|
288
|
+
get: (options?: RequestOptions) => Promise<Record<string, unknown>>;
|
|
289
|
+
/** Units of the current period, or of `YYYY-MM`, with a daily series and a breakdown per template. */
|
|
290
|
+
usage: (period?: string, options?: RequestOptions) => Promise<Usage>;
|
|
291
|
+
};
|
|
292
|
+
/** Raw request with auth, retries and problem mapping; use it for endpoints without a helper. */
|
|
293
|
+
request<T>(method: string, path: string, body?: unknown, options?: RequestOptions): Promise<T>;
|
|
294
|
+
private backoff;
|
|
295
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface VerifyOptions {
|
|
2
|
+
/** Maximum age of the timestamp in seconds (default 300). */
|
|
3
|
+
toleranceSeconds?: number;
|
|
4
|
+
/** Unix seconds "now"; tests pass a fixed clock. */
|
|
5
|
+
now?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Returns true when the header matches the raw body. Pass the body exactly as received (not
|
|
9
|
+
* re-serialised JSON): the signature covers the bytes on the wire.
|
|
10
|
+
*/
|
|
11
|
+
export declare function verifyWebhookSignature(secret: string, signatureHeader: string | null | undefined, rawBody: string, options?: VerifyOptions): Promise<boolean>;
|
|
12
|
+
/** Parses a verified event body. Throws when the signature does not match. */
|
|
13
|
+
export declare function parseWebhookEvent<T = {
|
|
14
|
+
id: string;
|
|
15
|
+
type: string;
|
|
16
|
+
created_at: string;
|
|
17
|
+
workspace_id: string | null;
|
|
18
|
+
data: unknown;
|
|
19
|
+
}>(secret: string, signatureHeader: string | null | undefined, rawBody: string, options?: VerifyOptions): Promise<T>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface VerifyOptions {
|
|
2
|
+
/** Maximum age of the timestamp in seconds (default 300). */
|
|
3
|
+
toleranceSeconds?: number;
|
|
4
|
+
/** Unix seconds "now"; tests pass a fixed clock. */
|
|
5
|
+
now?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Returns true when the header matches the raw body. Pass the body exactly as received (not
|
|
9
|
+
* re-serialised JSON): the signature covers the bytes on the wire.
|
|
10
|
+
*/
|
|
11
|
+
export declare function verifyWebhookSignature(secret: string, signatureHeader: string | null | undefined, rawBody: string, options?: VerifyOptions): Promise<boolean>;
|
|
12
|
+
/** Parses a verified event body. Throws when the signature does not match. */
|
|
13
|
+
export declare function parseWebhookEvent<T = {
|
|
14
|
+
id: string;
|
|
15
|
+
type: string;
|
|
16
|
+
created_at: string;
|
|
17
|
+
workspace_id: string | null;
|
|
18
|
+
data: unknown;
|
|
19
|
+
}>(secret: string, signatureHeader: string | null | undefined, rawBody: string, options?: VerifyOptions): Promise<T>;
|