uvpdftool 0.1.0 → 0.2.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/README.md +24 -3
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +11 -0
- package/dist/command.d.ts +13 -0
- package/dist/command.js +211 -0
- package/dist/files.d.ts +14 -0
- package/dist/files.js +77 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -2
- package/package.json +15 -10
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# uvpdftool
|
|
2
2
|
|
|
3
|
-
The [UV pdfTool API](https://uvpdftool.com/developers) from Node.js 18 and later
|
|
4
|
-
convert, OCR and check PDFs on UV pdfTool's servers. No dependencies; ES
|
|
5
|
-
included.
|
|
3
|
+
The [UV pdfTool API](https://uvpdftool.com/developers) from Node.js 18.3 and later, and from the
|
|
4
|
+
command line: compress, convert, OCR and check PDFs on UV pdfTool's servers. No dependencies; ES
|
|
5
|
+
modules; types included.
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
npm install uvpdftool
|
|
@@ -50,5 +50,26 @@ A refused request throws `ApiError` (`status`, `code`, `message`, `details`,
|
|
|
50
50
|
with `job.error`). A file is `{ name, data, contentType }` where `data` is a `Buffer`,
|
|
51
51
|
`Uint8Array`, `ArrayBuffer`, `Blob` or a `ReadableStream` with `size`.
|
|
52
52
|
|
|
53
|
+
## The command line
|
|
54
|
+
|
|
55
|
+
The package installs `uvpdftool`, which uploads the files, runs the tool and saves the results
|
|
56
|
+
beside them:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
npm install -g uvpdftool
|
|
60
|
+
export UVPDFTOOL_API_KEY=uvp_test_...
|
|
61
|
+
|
|
62
|
+
uvpdftool compress report.pdf --set method=ghostscript --set preset=ebook
|
|
63
|
+
uvpdftool office-to-pdf proposal.docx contract.docx -o pdfs
|
|
64
|
+
uvpdftool ocr scan.pdf --set 'languages=["eng","hin"]' --json
|
|
65
|
+
uvpdftool tools # what each tool takes, and its options
|
|
66
|
+
uvpdftool account # the key in use, and the API tasks left
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The key comes from `--key`, `UVPDFTOOL_API_KEY` or `UVPDF_KEY`. Each `--set` is one option of
|
|
70
|
+
the tool, read as JSON where it can be; `--options '{...}'` gives them all at once. A result never
|
|
71
|
+
replaces a file of the same name unless `--force` is given; a number is added instead. The exit
|
|
72
|
+
code is 0 when done, 1 when the API refused or the job failed, and 2 when the command was wrong.
|
|
73
|
+
|
|
53
74
|
The tools, their options and the errors are documented at
|
|
54
75
|
<https://uvpdftool.com/developers>.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { main } from './command.js';
|
|
3
|
+
// The command line itself: what it does is in command.ts, where the tests reach it.
|
|
4
|
+
void main(process.argv.slice(2), {
|
|
5
|
+
env: process.env,
|
|
6
|
+
cwd: process.cwd(),
|
|
7
|
+
stdout: (text) => process.stdout.write(text),
|
|
8
|
+
stderr: (text) => process.stderr.write(text),
|
|
9
|
+
}).then((code) => {
|
|
10
|
+
process.exitCode = code;
|
|
11
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** One `--set name=value`: the value as JSON where it parses, as text otherwise. */
|
|
2
|
+
export declare function parseSetting(setting: string): [string, unknown];
|
|
3
|
+
export interface Io {
|
|
4
|
+
env: Record<string, string | undefined>;
|
|
5
|
+
cwd: string;
|
|
6
|
+
stdout: (text: string) => void;
|
|
7
|
+
stderr: (text: string) => void;
|
|
8
|
+
/** Another fetch, for tests. */
|
|
9
|
+
fetch?: typeof fetch;
|
|
10
|
+
}
|
|
11
|
+
/** Runs the command line; resolves with the exit code: 0 done, 1 refused or failed, 2 misused. */
|
|
12
|
+
export { contentTypeOf, safeName } from './files.js';
|
|
13
|
+
export declare function main(argv: readonly string[], io: Io): Promise<number>;
|
package/dist/command.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { mkdir, stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
5
|
+
import { parseArgs } from 'node:util';
|
|
6
|
+
import { contentTypeOf, safeName, saveResult } from './files.js';
|
|
7
|
+
import { ApiError, JobFailedError, UvPdfTool, VERSION } from './index.js';
|
|
8
|
+
/**
|
|
9
|
+
* The UV pdfTool API from the command line: `uvpdftool <tool> <file>...` uploads the files,
|
|
10
|
+
* runs the tool and saves what it made; `uvpdftool tools` and `uvpdftool account` answer the
|
|
11
|
+
* two questions a script asks first. Nothing but Node itself and this package.
|
|
12
|
+
*/
|
|
13
|
+
const HELP = `uvpdftool ${VERSION}: the UV pdfTool API from the command line.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
uvpdftool <tool> <file>... [options] Run a tool on the files and save the results
|
|
17
|
+
uvpdftool tools The tools, what they take and their options
|
|
18
|
+
uvpdftool account The key in use and the tasks left
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
-k, --key <key> The API key; or set UVPDFTOOL_API_KEY (or UVPDF_KEY)
|
|
22
|
+
-o, --out <folder> Where to save the results (default: the current folder)
|
|
23
|
+
-s, --set <name=value> One option of the tool; repeat for more. Values are read as
|
|
24
|
+
JSON where they can be: --set preset=ebook --set linearize=true
|
|
25
|
+
--options <json> All the tool's options as one JSON object
|
|
26
|
+
--keep <hours> Keep the files on the server this long after the job, 1 to 24
|
|
27
|
+
--force Overwrite files of the same name instead of adding a number
|
|
28
|
+
--json Print the finished job as JSON
|
|
29
|
+
-q, --quiet No progress on standard error
|
|
30
|
+
-h, --help This help
|
|
31
|
+
-v, --version The version
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
uvpdftool compress report.pdf --set method=ghostscript --set preset=ebook
|
|
35
|
+
uvpdftool office-to-pdf proposal.docx -o pdfs
|
|
36
|
+
uvpdftool ocr scan.pdf --set 'languages=["eng","hin"]'
|
|
37
|
+
|
|
38
|
+
Tools, options and errors: https://uvpdftool.com/developers
|
|
39
|
+
`;
|
|
40
|
+
/** One `--set name=value`: the value as JSON where it parses, as text otherwise. */
|
|
41
|
+
export function parseSetting(setting) {
|
|
42
|
+
const at = setting.indexOf('=');
|
|
43
|
+
if (at < 1)
|
|
44
|
+
throw new UsageError(`--set needs name=value, not "${setting}".`);
|
|
45
|
+
const name = setting.slice(0, at).trim();
|
|
46
|
+
const raw = setting.slice(at + 1);
|
|
47
|
+
try {
|
|
48
|
+
return [name, JSON.parse(raw)];
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [name, raw];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
class UsageError extends Error {
|
|
55
|
+
}
|
|
56
|
+
/** Runs the command line; resolves with the exit code: 0 done, 1 refused or failed, 2 misused. */
|
|
57
|
+
export { contentTypeOf, safeName } from './files.js';
|
|
58
|
+
export async function main(argv, io) {
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = parseArgs({
|
|
62
|
+
args: [...argv],
|
|
63
|
+
allowPositionals: true,
|
|
64
|
+
options: {
|
|
65
|
+
key: { type: 'string', short: 'k' },
|
|
66
|
+
out: { type: 'string', short: 'o' },
|
|
67
|
+
set: { type: 'string', short: 's', multiple: true },
|
|
68
|
+
options: { type: 'string' },
|
|
69
|
+
keep: { type: 'string' },
|
|
70
|
+
force: { type: 'boolean' },
|
|
71
|
+
json: { type: 'boolean' },
|
|
72
|
+
quiet: { type: 'boolean', short: 'q' },
|
|
73
|
+
help: { type: 'boolean', short: 'h' },
|
|
74
|
+
version: { type: 'boolean', short: 'v' },
|
|
75
|
+
'base-url': { type: 'string' },
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
io.stderr(`${error instanceof Error ? error.message : String(error)}\n\n${HELP}`);
|
|
81
|
+
return 2;
|
|
82
|
+
}
|
|
83
|
+
const { values, positionals } = parsed;
|
|
84
|
+
if (values.version) {
|
|
85
|
+
io.stdout(`${VERSION}\n`);
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
const [command, ...files] = positionals;
|
|
89
|
+
if (values.help || command === undefined || command === 'help') {
|
|
90
|
+
io.stdout(HELP);
|
|
91
|
+
return command === undefined && !values.help ? 2 : 0;
|
|
92
|
+
}
|
|
93
|
+
const apiKey = values.key ?? io.env.UVPDFTOOL_API_KEY ?? io.env.UVPDF_KEY ?? '';
|
|
94
|
+
if (apiKey === '') {
|
|
95
|
+
io.stderr('An API key is needed: --key, or UVPDFTOOL_API_KEY. Make one at https://uvpdftool.com/developers/keys\n');
|
|
96
|
+
return 2;
|
|
97
|
+
}
|
|
98
|
+
const client = new UvPdfTool({
|
|
99
|
+
apiKey,
|
|
100
|
+
userAgent: 'uvpdftool-cli',
|
|
101
|
+
...(values['base-url'] ? { baseUrl: values['base-url'] } : {}),
|
|
102
|
+
...(io.fetch ? { fetch: io.fetch } : {}),
|
|
103
|
+
});
|
|
104
|
+
try {
|
|
105
|
+
if (command === 'tools') {
|
|
106
|
+
const tools = await client.tools();
|
|
107
|
+
if (values.json)
|
|
108
|
+
io.stdout(`${JSON.stringify(tools, null, 2)}\n`);
|
|
109
|
+
else {
|
|
110
|
+
for (const tool of tools) {
|
|
111
|
+
io.stdout(`${tool.id}\n takes ${tool.accepts.join(', ')}; up to ${String(tool.maxFiles)} files\n`);
|
|
112
|
+
const names = Object.keys(tool.options.properties ?? {});
|
|
113
|
+
if (names.length > 0)
|
|
114
|
+
io.stdout(` options: ${names.join(', ')}\n`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
if (command === 'account') {
|
|
120
|
+
const account = await client.account();
|
|
121
|
+
if (values.json)
|
|
122
|
+
io.stdout(`${JSON.stringify(account, null, 2)}\n`);
|
|
123
|
+
else {
|
|
124
|
+
io.stdout(`${account.key.name} (${account.key.mode}, ${account.key.prefix}…)\n` +
|
|
125
|
+
`API tasks left: ${String(account.tasks)}\n` +
|
|
126
|
+
`Test jobs left today: ${String(account.test.left)} of ${String(account.test.perDay)}\n`);
|
|
127
|
+
}
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
if (files.length === 0) {
|
|
131
|
+
// A tool that takes no files (web-to-pdf) runs on its options alone.
|
|
132
|
+
const known = (await client.tools()).find((tool) => tool.id === command);
|
|
133
|
+
if (!known || known.accepts.length > 0) {
|
|
134
|
+
throw new UsageError(`Name the files for ${command}.`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const options = {};
|
|
138
|
+
if (values.options !== undefined) {
|
|
139
|
+
let given;
|
|
140
|
+
try {
|
|
141
|
+
given = JSON.parse(values.options);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
throw new UsageError('--options must be a JSON object.');
|
|
145
|
+
}
|
|
146
|
+
if (typeof given !== 'object' || given === null || Array.isArray(given)) {
|
|
147
|
+
throw new UsageError('--options must be a JSON object.');
|
|
148
|
+
}
|
|
149
|
+
Object.assign(options, given);
|
|
150
|
+
}
|
|
151
|
+
for (const setting of values.set ?? []) {
|
|
152
|
+
const [name, value] = parseSetting(setting);
|
|
153
|
+
options[name] = value;
|
|
154
|
+
}
|
|
155
|
+
const keep = values.keep === undefined ? undefined : Number(values.keep);
|
|
156
|
+
if (keep !== undefined && (!Number.isInteger(keep) || keep < 1 || keep > 24)) {
|
|
157
|
+
throw new UsageError('--keep takes a whole number of hours, 1 to 24.');
|
|
158
|
+
}
|
|
159
|
+
const inputs = [];
|
|
160
|
+
for (const file of files) {
|
|
161
|
+
const full = path.resolve(io.cwd, file);
|
|
162
|
+
const info = await stat(full).catch(() => null);
|
|
163
|
+
if (!info?.isFile())
|
|
164
|
+
throw new UsageError(`${file}: no such file.`);
|
|
165
|
+
inputs.push({
|
|
166
|
+
name: path.basename(full),
|
|
167
|
+
contentType: contentTypeOf(full),
|
|
168
|
+
size: info.size,
|
|
169
|
+
data: Readable.toWeb(createReadStream(full)),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const quiet = values.quiet === true;
|
|
173
|
+
if (!quiet)
|
|
174
|
+
io.stderr(`${command}: uploading ${String(inputs.length)} file(s)…\n`);
|
|
175
|
+
const job = await client.run({
|
|
176
|
+
tool: command,
|
|
177
|
+
files: inputs,
|
|
178
|
+
...(Object.keys(options).length > 0 ? { options } : {}),
|
|
179
|
+
...(keep !== undefined ? { retentionHours: keep } : {}),
|
|
180
|
+
onProgress: (fraction) => {
|
|
181
|
+
if (!quiet)
|
|
182
|
+
io.stderr(`${command}: ${String(Math.round(fraction * 100))}%\n`);
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
const folder = path.resolve(io.cwd, values.out ?? '.');
|
|
186
|
+
await mkdir(folder, { recursive: true });
|
|
187
|
+
const saved = [];
|
|
188
|
+
for (const [index, output] of job.outputs.entries()) {
|
|
189
|
+
const data = await client.download(output);
|
|
190
|
+
saved.push(await saveResult(folder, safeName(output.name, index), data, values.force === true));
|
|
191
|
+
}
|
|
192
|
+
if (values.json)
|
|
193
|
+
io.stdout(`${JSON.stringify({ job, saved }, null, 2)}\n`);
|
|
194
|
+
else
|
|
195
|
+
for (const file of saved)
|
|
196
|
+
io.stdout(`${file}\n`);
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (error instanceof UsageError) {
|
|
201
|
+
io.stderr(`${error.message}\n`);
|
|
202
|
+
return 2;
|
|
203
|
+
}
|
|
204
|
+
if (error instanceof JobFailedError || error instanceof ApiError) {
|
|
205
|
+
io.stderr(`${error.message}\n`);
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
io.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
209
|
+
return 1;
|
|
210
|
+
}
|
|
211
|
+
}
|
package/dist/files.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const contentTypeOf: (file: string) => string;
|
|
2
|
+
/**
|
|
3
|
+
* A result's name as a file in the output folder: its last part only, with the characters no
|
|
4
|
+
* system allows in a name replaced, so a name from the server can never reach another folder.
|
|
5
|
+
*/
|
|
6
|
+
export declare function safeName(name: string, index: number): string;
|
|
7
|
+
/** A free path for `name` in `folder`: the name itself, or with " (2)", " (3)"… before the extension. */
|
|
8
|
+
export declare function freePath(folder: string, name: string, force: boolean): Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Saves a result as `name` in `folder`, under a free name unless `force`, and says where. The file
|
|
11
|
+
* is created only if nothing is there, so a result saved at the same moment, or a link put at the
|
|
12
|
+
* name, is never written over or through: the next free name is taken instead.
|
|
13
|
+
*/
|
|
14
|
+
export declare function saveResult(folder: string, name: string, data: Uint8Array, force: boolean): Promise<string>;
|
package/dist/files.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { lstat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Local files and the API: the content type to declare for a file, and a safe, free place to
|
|
5
|
+
* save a result, for the command line and for the MCP server alike.
|
|
6
|
+
*/
|
|
7
|
+
/** Content types by file extension, for what the tools take. */
|
|
8
|
+
const TYPES = {
|
|
9
|
+
'.pdf': 'application/pdf',
|
|
10
|
+
'.doc': 'application/msword',
|
|
11
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
12
|
+
'.xls': 'application/vnd.ms-excel',
|
|
13
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
14
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
15
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
16
|
+
'.odt': 'application/vnd.oasis.opendocument.text',
|
|
17
|
+
'.ods': 'application/vnd.oasis.opendocument.spreadsheet',
|
|
18
|
+
'.odp': 'application/vnd.oasis.opendocument.presentation',
|
|
19
|
+
'.rtf': 'application/rtf',
|
|
20
|
+
'.txt': 'text/plain',
|
|
21
|
+
'.csv': 'text/csv',
|
|
22
|
+
'.html': 'text/html',
|
|
23
|
+
'.htm': 'text/html',
|
|
24
|
+
'.md': 'text/markdown',
|
|
25
|
+
'.markdown': 'text/markdown',
|
|
26
|
+
'.epub': 'application/epub+zip',
|
|
27
|
+
'.png': 'image/png',
|
|
28
|
+
'.jpg': 'image/jpeg',
|
|
29
|
+
'.jpeg': 'image/jpeg',
|
|
30
|
+
};
|
|
31
|
+
export const contentTypeOf = (file) => TYPES[path.extname(file).toLowerCase()] ?? 'application/octet-stream';
|
|
32
|
+
const RESERVED = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/iu;
|
|
33
|
+
/**
|
|
34
|
+
* A result's name as a file in the output folder: its last part only, with the characters no
|
|
35
|
+
* system allows in a name replaced, so a name from the server can never reach another folder.
|
|
36
|
+
*/
|
|
37
|
+
export function safeName(name, index) {
|
|
38
|
+
const last = name.split(/[\\/]/u).pop() ?? '';
|
|
39
|
+
// eslint-disable-next-line no-control-regex -- control characters are what is being removed
|
|
40
|
+
const cleaned = last.replace(/[<>:"|?*\u0000-\u001f]/gu, '_').replace(/^[.\s]+|[.\s]+$/gu, '');
|
|
41
|
+
if (cleaned === '')
|
|
42
|
+
return `result-${String(index + 1)}`;
|
|
43
|
+
return RESERVED.test(cleaned) ? `_${cleaned}` : cleaned;
|
|
44
|
+
}
|
|
45
|
+
// lstat: a link at the name counts as taken, even one that leads nowhere.
|
|
46
|
+
const exists = async (file) => lstat(file).then(() => true, () => false);
|
|
47
|
+
/** A free path for `name` in `folder`: the name itself, or with " (2)", " (3)"… before the extension. */
|
|
48
|
+
export async function freePath(folder, name, force) {
|
|
49
|
+
const first = path.join(folder, name);
|
|
50
|
+
if (force || !(await exists(first)))
|
|
51
|
+
return first;
|
|
52
|
+
const extension = path.extname(name);
|
|
53
|
+
const stem = name.slice(0, name.length - extension.length);
|
|
54
|
+
for (let count = 2;; count += 1) {
|
|
55
|
+
const candidate = path.join(folder, `${stem} (${String(count)})${extension}`);
|
|
56
|
+
if (!(await exists(candidate)))
|
|
57
|
+
return candidate;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Saves a result as `name` in `folder`, under a free name unless `force`, and says where. The file
|
|
62
|
+
* is created only if nothing is there, so a result saved at the same moment, or a link put at the
|
|
63
|
+
* name, is never written over or through: the next free name is taken instead.
|
|
64
|
+
*/
|
|
65
|
+
export async function saveResult(folder, name, data, force) {
|
|
66
|
+
for (;;) {
|
|
67
|
+
const target = await freePath(folder, name, force);
|
|
68
|
+
try {
|
|
69
|
+
await writeFile(target, data, { flag: force ? 'w' : 'wx' });
|
|
70
|
+
return target;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (force || error.code !== 'EEXIST')
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { contentTypeOf, freePath, safeName, saveResult } from './files.js';
|
|
1
2
|
/**
|
|
2
3
|
* The UV pdfTool API from Node.js 18 and later, with nothing to install besides this
|
|
3
4
|
* package: create a job, upload the files, start it, wait for it, download the result, or
|
|
@@ -119,6 +120,7 @@ export declare class JobFailedError extends Error {
|
|
|
119
120
|
readonly job: Job;
|
|
120
121
|
constructor(job: Job);
|
|
121
122
|
}
|
|
123
|
+
export declare const VERSION = "0.2.0";
|
|
122
124
|
export declare class UvPdfTool {
|
|
123
125
|
readonly jobs: {
|
|
124
126
|
create: (request: CreateJobRequest) => Promise<{
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
export { contentTypeOf, freePath, safeName, saveResult } from './files.js';
|
|
2
3
|
/**
|
|
3
4
|
* The UV pdfTool API from Node.js 18 and later, with nothing to install besides this
|
|
4
5
|
* package: create a job, upload the files, start it, wait for it, download the result, or
|
|
@@ -33,7 +34,7 @@ export class JobFailedError extends Error {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
const ENDED = ['done', 'failed', 'cancelled'];
|
|
36
|
-
const VERSION = '0.
|
|
37
|
+
export const VERSION = '0.2.0';
|
|
37
38
|
const sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
38
39
|
const timer = setTimeout(resolve, ms);
|
|
39
40
|
signal?.addEventListener('abort', () => {
|
|
@@ -236,5 +237,6 @@ export function verifyWebhook(rawBody, signatureHeader, secret, toleranceSeconds
|
|
|
236
237
|
.update(body)
|
|
237
238
|
.digest('hex');
|
|
238
239
|
const given = parts.get('v1') ?? '';
|
|
239
|
-
|
|
240
|
+
// Hex only: then both are the same length in bytes, as timingSafeEqual needs.
|
|
241
|
+
return (/^[0-9a-f]{64}$/u.test(given) && timingSafeEqual(Buffer.from(given), Buffer.from(expected)));
|
|
240
242
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uvpdftool",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The UV pdfTool API from Node.js: compress, convert, OCR and check PDFs on UV pdfTool's servers.",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "The UV pdfTool API from Node.js and the command line: compress, convert, OCR and check PDFs on UV pdfTool's servers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "UV ResearchLabs",
|
|
7
7
|
"homepage": "https://uvpdftool.com/developers",
|
|
@@ -17,32 +17,31 @@
|
|
|
17
17
|
"convert",
|
|
18
18
|
"pdfa",
|
|
19
19
|
"api",
|
|
20
|
+
"cli",
|
|
20
21
|
"uvpdftool"
|
|
21
22
|
],
|
|
22
23
|
"type": "module",
|
|
23
24
|
"sideEffects": false,
|
|
24
25
|
"engines": {
|
|
25
|
-
"node": ">=18"
|
|
26
|
+
"node": ">=18.3"
|
|
26
27
|
},
|
|
27
28
|
"exports": {
|
|
28
29
|
".": {
|
|
30
|
+
"@pdftool/source": "./src/index.ts",
|
|
29
31
|
"types": "./dist/index.d.ts",
|
|
30
32
|
"import": "./dist/index.js"
|
|
31
33
|
},
|
|
32
34
|
"./package.json": "./package.json"
|
|
33
35
|
},
|
|
34
36
|
"types": "./dist/index.d.ts",
|
|
37
|
+
"bin": {
|
|
38
|
+
"uvpdftool": "./dist/cli.js"
|
|
39
|
+
},
|
|
35
40
|
"files": [
|
|
36
41
|
"dist",
|
|
37
42
|
"README.md",
|
|
38
43
|
"LICENSE"
|
|
39
44
|
],
|
|
40
|
-
"scripts": {
|
|
41
|
-
"build": "tsc -p tsconfig.build.json",
|
|
42
|
-
"test": "vitest run",
|
|
43
|
-
"lint": "eslint .",
|
|
44
|
-
"typecheck": "tsc -p tsconfig.json"
|
|
45
|
-
},
|
|
46
45
|
"devDependencies": {
|
|
47
46
|
"@eslint/js": "^10.0.1",
|
|
48
47
|
"@types/node": "^24.13.6",
|
|
@@ -50,5 +49,11 @@
|
|
|
50
49
|
"typescript": "~6.0.3",
|
|
51
50
|
"typescript-eslint": "^8.70.0",
|
|
52
51
|
"vitest": "^4.1.12"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsc -p tsconfig.build.json",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"lint": "eslint .",
|
|
57
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
53
58
|
}
|
|
54
|
-
}
|
|
59
|
+
}
|