@nucleushq/marqpdf 0.0.0-stage → 0.3.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 CHANGED
@@ -1,3 +1,60 @@
1
- # Temporary Holding Version
1
+ # @nucleushq/marqpdf
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The [MarqPDF API](https://marqpdf.com/developers) from JavaScript: compress PDFs to a target size, OCR scans (English and Arabic), convert to and from Word, Excel and PowerPoint, merge, split, rotate, watermark, protect and extract text.
4
+
5
+ No dependencies. Node 18+, Deno, Bun.
6
+
7
+ ```sh
8
+ npm install @nucleushq/marqpdf
9
+ ```
10
+
11
+ Create an API key at [marqpdf.com → Account → Developers](https://marqpdf.com/?dev=keys) and set `MARQPDF_API_KEY`.
12
+
13
+ ```js
14
+ import { MarqPDF } from "@nucleushq/marqpdf";
15
+
16
+ const marq = new MarqPDF(); // reads MARQPDF_API_KEY
17
+
18
+ // Inputs can be a local path, a URL, bytes, a Blob, or a file id from an earlier step.
19
+ const job = await marq.compress("contract.pdf", { target_mb: 1 });
20
+ await marq.download(job, "contract-small.pdf");
21
+
22
+ // Steps chain on file ids.
23
+ const scanned = await marq.ocr("scan.pdf");
24
+ const small = await marq.compress(scanned.output.files[0], { target_mb: 2 });
25
+ console.log(small.output.files[0].download_url); // works without a key for an hour
26
+
27
+ const text = await marq.extractText("https://example.com/statement.pdf");
28
+ console.log(text.output.text);
29
+
30
+ // AI: fields as JSON, on MarqPDF's model or your own ("anthropic/claude-opus-5-5", …).
31
+ const inv = await marq.extract("invoice.pdf", { fields: ["invoice_number", { name: "total", type: "number" }, { name: "due_date", type: "date" }] });
32
+ console.log(inv.output.data); // { invoice_number: "INV-2041", total: 5136, due_date: "2025-04-13" }
33
+ const a = await marq.ask("lease.pdf", { question: "When can the tenant terminate early?" });
34
+
35
+ // Workflows: save a chain once, run it on any file.
36
+ const wf = await marq.workflows.create("Scan to small PDF", [{ operation: "ocr" }, { operation: "compress", options: { target_mb: 1 } }]);
37
+ const done = await marq.workflows.run(wf.id, "scan.pdf");
38
+
39
+ // The agent: a goal in plain words; it picks the operations.
40
+ const job = await marq.agent(["jan.pdf", "feb.pdf"], { goal: "Merge these and get the result under 2 MB" });
41
+ console.log(job.output.text, job.output.steps);
42
+ ```
43
+
44
+ Every operation: `compress`, `merge`, `split`, `rotate`, `organize`, `removePages`, `pageNumbers`, `watermark`, `protect`, `unlock`, `ocr`, `convert`, `imagesToPdf`, `compare`, `extractText`, the AI ones `extract`, `ask` and `agent`, or `run(operation, input, options)`. Each resolves with the finished job and throws `MarqPDFError` (with `status`, `code` and the `job`) if it fails.
45
+
46
+ Also: `files.upload / list / get / delete / content`, `jobs.get / list / wait`, `workflows.create / list / get / delete / run / runSteps`, `models()`, `usage()`.
47
+
48
+ ## Webhooks
49
+
50
+ ```js
51
+ import { verifyWebhook } from "@nucleushq/marqpdf";
52
+
53
+ app.post("/hooks/marqpdf", express.raw({ type: "application/json" }), async (req, res) => {
54
+ if (!(await verifyWebhook(req.body, req.get("MarqPDF-Signature"), process.env.MARQPDF_WEBHOOK_SECRET))) return res.sendStatus(400);
55
+ const { type, data: job } = JSON.parse(req.body);
56
+ res.sendStatus(200);
57
+ });
58
+ ```
59
+
60
+ Free accounts include 100 operations a month; a Week Pass, Pro or Team plan is unlimited. Files are deleted after 24 hours. Full reference: https://marqpdf.com/developers
package/marqpdf.d.ts ADDED
@@ -0,0 +1,121 @@
1
+ // Types for @nucleushq/marqpdf. The API reference: https://marqpdf.com/developers
2
+
3
+ export type FileInput = string | { id: string } | ArrayBuffer | ArrayBufferView | Blob;
4
+
5
+ export interface MarqFile {
6
+ object: "file";
7
+ id: string;
8
+ name: string;
9
+ size: number;
10
+ content_type: string;
11
+ pages?: number;
12
+ created_at: string;
13
+ expires_at: string;
14
+ source: "upload" | "url" | "job";
15
+ job?: string;
16
+ /** Works without a key for one hour. */
17
+ download_url: string;
18
+ }
19
+
20
+ export interface Job {
21
+ object: "job";
22
+ id: string;
23
+ operation: string;
24
+ status: "queued" | "running" | "succeeded" | "failed";
25
+ created_at: string;
26
+ started_at: string | null;
27
+ finished_at: string | null;
28
+ input: { files: string[]; options: Record<string, unknown> };
29
+ output: { files: MarqFile[]; text?: string; text_truncated?: boolean; note?: string; data?: Record<string, unknown>; model?: string; steps?: Record<string, unknown>[] } | null;
30
+ error: { code: string; message: string } | null;
31
+ webhook_url: string | null;
32
+ }
33
+
34
+ export interface Workflow {
35
+ object: "workflow";
36
+ id: string;
37
+ name: string;
38
+ steps: { operation: string; options: Record<string, unknown> }[];
39
+ created_at: string;
40
+ }
41
+
42
+ export interface Usage {
43
+ object: "usage";
44
+ month: string;
45
+ operations: number;
46
+ /** null = unlimited. */
47
+ limit: number | null;
48
+ plan: "free" | "week" | "pro" | "team";
49
+ }
50
+
51
+ export interface RunOptions {
52
+ /** Seconds the request waits before the SDK polls instead (default 60, max 300). */
53
+ wait?: number;
54
+ webhook_url?: string;
55
+ [option: string]: unknown;
56
+ }
57
+
58
+ export class MarqPDFError extends Error {
59
+ status?: number;
60
+ code?: string;
61
+ job?: Job;
62
+ body?: unknown;
63
+ }
64
+
65
+ export const VERSION: string;
66
+ export const OPERATIONS: Record<string, string>;
67
+
68
+ export class MarqPDF {
69
+ constructor(opts?: { apiKey?: string; baseUrl?: string; fetch?: typeof fetch; timeoutMs?: number });
70
+ files: {
71
+ upload(input: FileInput, opts?: { name?: string }): Promise<MarqFile>;
72
+ list(opts?: { limit?: number }): Promise<MarqFile[]>;
73
+ get(id: string): Promise<MarqFile>;
74
+ delete(id: string): Promise<{ id: string; deleted: true }>;
75
+ content(id: string): Promise<ArrayBuffer>;
76
+ };
77
+ jobs: {
78
+ get(id: string): Promise<Job>;
79
+ list(opts?: { limit?: number }): Promise<Job[]>;
80
+ wait(job: string | Job, opts?: { intervalMs?: number; timeoutMs?: number }): Promise<Job>;
81
+ };
82
+ usage(): Promise<Usage>;
83
+ run(operation: string, input: FileInput | FileInput[], options?: RunOptions): Promise<Job>;
84
+ download(jobOrFile: Job | MarqFile | string, path?: string): Promise<string>;
85
+
86
+ compress(input: FileInput, options?: RunOptions & { target_mb?: number; level?: "screen" | "ebook" | "printer" | "prepress" }): Promise<Job>;
87
+ merge(inputs: FileInput[], options?: RunOptions): Promise<Job>;
88
+ split(input: FileInput, options?: RunOptions & { every?: number; ranges?: [number, number][] }): Promise<Job>;
89
+ rotate(input: FileInput, options: RunOptions & { angle: 90 | 180 | 270; pages?: number[] }): Promise<Job>;
90
+ organize(input: FileInput, options: RunOptions & { pages: number[] }): Promise<Job>;
91
+ removePages(input: FileInput, options: RunOptions & { pages: number[] }): Promise<Job>;
92
+ pageNumbers(input: FileInput, options?: RunOptions & { position?: string }): Promise<Job>;
93
+ watermark(input: FileInput, options: RunOptions & { text: string }): Promise<Job>;
94
+ protect(input: FileInput, options: RunOptions & { password: string }): Promise<Job>;
95
+ unlock(input: FileInput, options?: RunOptions & { password?: string }): Promise<Job>;
96
+ ocr(input: FileInput, options?: RunOptions): Promise<Job>;
97
+ convert(input: FileInput, options: RunOptions & { to: "pdf" | "docx" | "xlsx" | "pptx" | "jpg" }): Promise<Job>;
98
+ imagesToPdf(inputs: FileInput[], options?: RunOptions): Promise<Job>;
99
+ compare(inputs: [FileInput, FileInput], options?: RunOptions): Promise<Job>;
100
+ extractText(input: FileInput, options?: RunOptions & { ocr?: "auto" | "always" | "never" }): Promise<Job>;
101
+ /** AI: the fields you name (or a JSON Schema) as JSON, in job.output.data. */
102
+ extract(input: FileInput, options: RunOptions & { fields?: (string | { name: string; type?: "string" | "number" | "integer" | "boolean" | "date" | "list"; description?: string })[]; schema?: object; instructions?: string; model?: string }): Promise<Job>;
103
+ /** AI: an answer from the document, in job.output.text. */
104
+ ask(input: FileInput, options: RunOptions & { question: string; instructions?: string; model?: string }): Promise<Job>;
105
+ /** AI: give a goal; the agent picks and runs operations. Summary in job.output.text, trace in job.output.steps. */
106
+ agent(inputs: FileInput | FileInput[], options: RunOptions & { goal: string; max_steps?: number; model?: string }): Promise<Job>;
107
+ workflows: {
108
+ create(name: string, steps: { operation: string; options?: Record<string, unknown> }[]): Promise<Workflow>;
109
+ list(): Promise<Workflow[]>;
110
+ get(id: string): Promise<Workflow>;
111
+ delete(id: string): Promise<{ id: string; deleted: true }>;
112
+ run(id: string, input: FileInput | FileInput[], options?: RunOptions): Promise<Job>;
113
+ runSteps(steps: { operation: string; options?: Record<string, unknown> }[], input: FileInput | FileInput[], options?: RunOptions): Promise<Job>;
114
+ };
115
+ /** Models this account can name in `model`. */
116
+ models(): Promise<{ object: "list"; data: object[]; pipelines: Record<string, string> }>;
117
+ }
118
+
119
+ export function verifyWebhook(rawBody: string | Uint8Array, signatureHeader: string, secret: string, opts?: { toleranceSec?: number }): Promise<boolean>;
120
+
121
+ export default MarqPDF;
package/marqpdf.js ADDED
@@ -0,0 +1,233 @@
1
+ // @nucleushq/marqpdf: the MarqPDF API (https://marqpdf.com/developers) from JavaScript.
2
+ // No dependencies. Node 18+, Deno and Bun; browsers too, though an API key
3
+ // belongs on a server, never in a page.
4
+
5
+ const DEFAULT_BASE = "https://marqpdf.com";
6
+ export const VERSION = "0.3.0";
7
+
8
+ /** Every operation: method name → API path. */
9
+ export const OPERATIONS = {
10
+ compress: "compress",
11
+ merge: "merge",
12
+ split: "split",
13
+ rotate: "rotate",
14
+ organize: "organize",
15
+ removePages: "remove-pages",
16
+ pageNumbers: "page-numbers",
17
+ watermark: "watermark",
18
+ protect: "protect",
19
+ unlock: "unlock",
20
+ ocr: "ocr",
21
+ convert: "convert",
22
+ imagesToPdf: "images-to-pdf",
23
+ compare: "compare",
24
+ extractText: "extract-text",
25
+ extract: "extract",
26
+ ask: "ask",
27
+ agent: "agent",
28
+ };
29
+ const MULTI = new Set(["merge", "images-to-pdf", "compare", "agent"]);
30
+
31
+ export class MarqPDFError extends Error {
32
+ constructor(message, { status, code, job, body } = {}) {
33
+ super(message);
34
+ this.name = "MarqPDFError";
35
+ this.status = status;
36
+ this.code = code;
37
+ this.job = job;
38
+ this.body = body;
39
+ }
40
+ }
41
+
42
+ const isNode = typeof process !== "undefined" && !!process.versions?.node;
43
+ const env = (k) => (isNode ? process.env[k] : undefined);
44
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
45
+
46
+ export class MarqPDF {
47
+ /**
48
+ * @param {{apiKey?: string, baseUrl?: string, fetch?: typeof fetch, timeoutMs?: number}} [opts]
49
+ */
50
+ constructor({ apiKey = env("MARQPDF_API_KEY"), baseUrl = env("MARQPDF_BASE_URL") || DEFAULT_BASE, fetch: f, timeoutMs = 600_000 } = {}) {
51
+ if (!apiKey) throw new MarqPDFError("No API key: pass { apiKey } or set MARQPDF_API_KEY. Create one at https://marqpdf.com/?dev=keys");
52
+ this.apiKey = apiKey;
53
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
54
+ this._fetch = f || globalThis.fetch.bind(globalThis);
55
+ this.timeoutMs = timeoutMs;
56
+
57
+ this.files = {
58
+ /** Upload a local path (Node), a URL, bytes, a Blob/File, or pass a file id through. */
59
+ upload: (input, opts) => this._upload(input, opts),
60
+ list: ({ limit } = {}) => this._req("GET", "/v1/files", { query: { limit } }).then((r) => r.data),
61
+ get: (id) => this._req("GET", `/v1/files/${encodeURIComponent(id)}`),
62
+ delete: (id) => this._req("DELETE", `/v1/files/${encodeURIComponent(id)}`),
63
+ /** The file's bytes, as an ArrayBuffer. */
64
+ content: (id) => this._req("GET", `/v1/files/${encodeURIComponent(id)}/content`, { raw: true }).then((r) => r.arrayBuffer()),
65
+ };
66
+ this.jobs = {
67
+ get: (id) => this._req("GET", `/v1/jobs/${encodeURIComponent(id)}`),
68
+ list: ({ limit } = {}) => this._req("GET", "/v1/jobs", { query: { limit } }).then((r) => r.data),
69
+ /** Poll until a job finishes; resolves with it, or throws MarqPDFError if it failed. */
70
+ wait: (job, opts) => this._wait(job, opts),
71
+ };
72
+ this.workflows = {
73
+ /** Save a chain of steps: [{ operation: "ocr" }, { operation: "compress", options: { target_mb: 1 } }]. */
74
+ create: (name, steps) => this._req("POST", "/v1/workflows", { json: { name, steps } }),
75
+ list: () => this._req("GET", "/v1/workflows").then((r) => r.data),
76
+ get: (id) => this._req("GET", `/v1/workflows/${encodeURIComponent(id)}`),
77
+ delete: (id) => this._req("DELETE", `/v1/workflows/${encodeURIComponent(id)}`),
78
+ /** Run a saved workflow on a file (or files); resolves with the finished job. */
79
+ run: (id, input, options) => this._runAt(`/v1/workflows/${encodeURIComponent(id)}/run`, input, options, false),
80
+ /** Run steps without saving them. */
81
+ runSteps: (steps, input, options = {}) => this._runAt("/v1/workflows/run", input, { ...options, steps }, false),
82
+ };
83
+ for (const [method, op] of Object.entries(OPERATIONS)) {
84
+ this[method] = (input, options) => this.run(op, input, options);
85
+ }
86
+ }
87
+
88
+ /** Models this account can name in `model`, and each pipeline's choice. */
89
+ models() {
90
+ return this._req("GET", "/v1/models");
91
+ }
92
+
93
+ /** This month's operations and the plan's limit (null = unlimited). */
94
+ usage() {
95
+ return this._req("GET", "/v1/usage");
96
+ }
97
+
98
+ /**
99
+ * Run an operation and wait for it. `input` is one file (id, path, URL,
100
+ * bytes, Blob) or, for merge / images-to-pdf / compare, a list of them.
101
+ * Resolves with the finished job; its `output.files` hold the results.
102
+ */
103
+ async run(op, input, options = {}) {
104
+ return this._runAt(`/v1/${op}`, input, options, MULTI.has(op));
105
+ }
106
+
107
+ async _runAt(path, input, options = {}, multi = false) {
108
+ const { wait = 60, ...opts } = options;
109
+ const list = Array.isArray(input) ? input : [input];
110
+ const ids = [];
111
+ for (const i of list) ids.push(await this._fileId(i));
112
+ const body = multi || ids.length > 1 ? { files: ids, ...opts } : { file: ids[0], ...opts };
113
+ const res = await this._req("POST", path, { json: { ...body, wait }, allowJob: true });
114
+ return res.status === "succeeded" ? res : this._wait(res);
115
+ }
116
+
117
+ /**
118
+ * Node: save a job's first output (or a file object / id) to `path`.
119
+ * With no path, saves under the output's own name in the current directory.
120
+ * Resolves with the path written.
121
+ */
122
+ async download(jobOrFile, path) {
123
+ if (!isNode) throw new MarqPDFError("download() writes to disk, so it needs Node; use files.content(id) in a browser.");
124
+ const f = typeof jobOrFile === "string" ? { id: jobOrFile } : jobOrFile?.object === "job" ? jobOrFile.output?.files?.[0] : jobOrFile;
125
+ if (!f?.id) throw new MarqPDFError("Nothing to download: pass a finished job, a file, or a file id.");
126
+ const meta = f.name ? f : await this.files.get(f.id);
127
+ const { writeFile } = await import("node:fs/promises");
128
+ const out = path || meta.name;
129
+ await writeFile(out, Buffer.from(await this.files.content(f.id)));
130
+ return out;
131
+ }
132
+
133
+ // ── internals ──
134
+
135
+ async _fileId(input) {
136
+ if (typeof input === "string" && input.startsWith("file_")) return input;
137
+ if (input && typeof input === "object" && typeof input.id === "string" && input.id.startsWith("file_")) return input.id;
138
+ return (await this._upload(input)).id;
139
+ }
140
+
141
+ async _upload(input, { name } = {}) {
142
+ if (typeof input === "string" && /^https?:\/\//i.test(input)) {
143
+ return this._req("POST", "/v1/files", { json: { url: input, ...(name ? { name } : {}) } });
144
+ }
145
+ let bytes, fname = name;
146
+ if (typeof input === "string") {
147
+ if (input.startsWith("file_")) return this.files.get(input);
148
+ if (!isNode) throw new MarqPDFError("A path can only be read in Node; pass a Blob or bytes instead.");
149
+ const [{ readFile }, { basename }] = await Promise.all([import("node:fs/promises"), import("node:path")]);
150
+ bytes = await readFile(input);
151
+ fname = fname || basename(input);
152
+ } else if (typeof Blob !== "undefined" && input instanceof Blob) {
153
+ bytes = input;
154
+ fname = fname || input.name || "document.pdf";
155
+ } else if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) {
156
+ bytes = input;
157
+ fname = fname || "document.pdf";
158
+ } else {
159
+ throw new MarqPDFError("Can't upload that: pass a file id, a path, a URL, bytes or a Blob.");
160
+ }
161
+ return this._req("POST", "/v1/files", { body: bytes, query: { name: fname }, headers: { "Content-Type": "application/octet-stream" } });
162
+ }
163
+
164
+ async _wait(job, { intervalMs = 1500, timeoutMs = 30 * 60_000 } = {}) {
165
+ const id = typeof job === "string" ? job : job.id;
166
+ const until = Date.now() + timeoutMs;
167
+ let j = typeof job === "string" ? await this.jobs.get(id) : job;
168
+ while (j.status === "queued" || j.status === "running") {
169
+ if (Date.now() > until) throw new MarqPDFError(`Job ${id} is still ${j.status}; check it later with jobs.get().`, { job: j });
170
+ await sleep(intervalMs);
171
+ j = await this.jobs.get(id);
172
+ }
173
+ if (j.status === "failed") throw new MarqPDFError(j.error?.message || "The operation failed.", { code: j.error?.code, job: j });
174
+ return j;
175
+ }
176
+
177
+ async _req(method, path, { json, body, query, headers = {}, raw = false, allowJob = false } = {}) {
178
+ const url = new URL(this.baseUrl + path);
179
+ for (const [k, v] of Object.entries(query || {})) if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
180
+ const h = { Authorization: `Bearer ${this.apiKey}`, "User-Agent": `marqpdf-js/${VERSION}`, ...headers };
181
+ let payload = body;
182
+ if (json !== undefined) {
183
+ h["Content-Type"] = "application/json";
184
+ payload = JSON.stringify(json);
185
+ }
186
+ const ctrl = new AbortController();
187
+ const t = setTimeout(() => ctrl.abort(), this.timeoutMs);
188
+ let res;
189
+ try {
190
+ res = await this._fetch(url, { method, headers: h, body: payload, signal: ctrl.signal });
191
+ } catch (e) {
192
+ throw new MarqPDFError(`Couldn't reach MarqPDF: ${e.message}`);
193
+ } finally {
194
+ clearTimeout(t);
195
+ }
196
+ if (raw && res.ok) return res;
197
+ const text = await res.text();
198
+ let data;
199
+ try {
200
+ data = text ? JSON.parse(text) : {};
201
+ } catch {
202
+ data = { error: { message: text } };
203
+ }
204
+ if (res.ok || (allowJob && res.status === 202)) return data;
205
+ const e = data.error || {};
206
+ throw new MarqPDFError(e.message || `MarqPDF answered ${res.status}`, { status: res.status, code: e.code, job: data.job, body: data });
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Check a webhook's MarqPDF-Signature header. `rawBody` must be the exact
212
+ * bytes received (a string or Uint8Array), before any JSON parsing.
213
+ * Resolves true when the signature matches and is fresh.
214
+ */
215
+ export async function verifyWebhook(rawBody, signatureHeader, secret, { toleranceSec = 300 } = {}) {
216
+ if (!signatureHeader || !secret) return false;
217
+ const parts = Object.fromEntries(String(signatureHeader).split(",").map((p) => p.trim().split("=", 2)));
218
+ const t = Number(parts.t);
219
+ if (!parts.v1 || !Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
220
+ const enc = new TextEncoder();
221
+ const body = typeof rawBody === "string" ? enc.encode(rawBody) : new Uint8Array(rawBody);
222
+ const msg = new Uint8Array([...enc.encode(`${t}.`), ...body]);
223
+ const subtle = globalThis.crypto?.subtle || (await import("node:crypto")).webcrypto.subtle;
224
+ const key = await subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
225
+ const mac = new Uint8Array(await subtle.sign("HMAC", key, msg));
226
+ const want = [...mac].map((b) => b.toString(16).padStart(2, "0")).join("");
227
+ if (want.length !== parts.v1.length) return false;
228
+ let diff = 0;
229
+ for (let i = 0; i < want.length; i++) diff |= want.charCodeAt(i) ^ parts.v1.charCodeAt(i);
230
+ return diff === 0;
231
+ }
232
+
233
+ export default MarqPDF;
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@nucleushq/marqpdf",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.0",
4
+ "description": "MarqPDF API client: compress, OCR, convert, merge, split and protect PDFs from code.",
5
+ "type": "module",
6
+ "main": "marqpdf.js",
7
+ "types": "marqpdf.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./marqpdf.d.ts",
11
+ "default": "./marqpdf.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "marqpdf.js",
16
+ "marqpdf.d.ts",
17
+ "README.md"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "license": "UNLICENSED",
23
+ "homepage": "https://marqpdf.com/developers",
24
+ "keywords": [
25
+ "pdf",
26
+ "compress-pdf",
27
+ "ocr",
28
+ "pdf-to-word",
29
+ "merge-pdf",
30
+ "api",
31
+ "sdk",
32
+ "mcp"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }