@office2pdf/node 0.1.2

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 ADDED
@@ -0,0 +1,129 @@
1
+ # office2pdf (Node.js)
2
+
3
+ Official Node.js SDK for the Office2PDF API.
4
+
5
+ ![npm](https://img.shields.io/npm/v/@politehq/office2pdf)
6
+ ![downloads](https://img.shields.io/npm/dm/@politehq/office2pdf)
7
+ ![license](https://img.shields.io/npm/l/@politehq/office2pdf)
8
+
9
+ Convert Word, Excel, and PowerPoint documents to PDF with a simple, production-ready API.
10
+
11
+ ---
12
+
13
+ ## Requirements
14
+
15
+ - Node.js **>= 18**
16
+ - An Office2PDF API key
17
+
18
+ Authentication is done via the `x-api-key` request header.
19
+
20
+ ---
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install @politehq/office2pdf
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Quick start
31
+
32
+ ```ts
33
+ import { Office2PDF } from "@politehq/office2pdf";
34
+
35
+ const client = new Office2PDF({
36
+ apiKey: process.env.OFFICE2PDF_API_KEY!,
37
+ });
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Convert and download to file (recommended)
43
+
44
+ For production workloads and large documents, always download directly to disk.
45
+
46
+ ```ts
47
+ const result = await client.convert({
48
+ filePath: "./input.docx",
49
+ downloadToPath: "./output.pdf",
50
+ });
51
+
52
+ console.log("Saved to:", result.path);
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Convert to in-memory buffer
58
+
59
+ Suitable for small files or quick scripts.
60
+
61
+ ```ts
62
+ import fs from "node:fs";
63
+
64
+ const result = await client.convert({
65
+ filePath: "./input.docx",
66
+ });
67
+
68
+ if (result.kind === "buffer") {
69
+ fs.writeFileSync("./output.pdf", result.buffer);
70
+ }
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Streaming output
76
+
77
+ For advanced use-cases, you can receive a Web ReadableStream.
78
+
79
+ ```ts
80
+ const result = await client.convert({
81
+ filePath: "./input.docx",
82
+ asWebStream: true,
83
+ });
84
+
85
+ if (result.kind === "stream") {
86
+ // pipe or process the stream
87
+ }
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Error handling
93
+
94
+ All errors are thrown as `Office2PDFError` with stable error codes.
95
+
96
+ ```ts
97
+ import { Office2PDFError } from "office2pdf";
98
+
99
+ try {
100
+ await client.convert({ filePath: "./input.docx" });
101
+ } catch (e) {
102
+ if (e instanceof Office2PDFError) {
103
+ console.error(e.code, e.message, e.requestId);
104
+ }
105
+ }
106
+ ```
107
+
108
+ Common error codes include:
109
+
110
+ - `UNAUTHORIZED`
111
+ - `INVALID_REQUEST`
112
+ - `RATE_LIMITED`
113
+ - `QUOTA_EXCEEDED`
114
+ - `TIMEOUT`
115
+ - `SERVER_ERROR`
116
+
117
+ ---
118
+
119
+ ## Notes
120
+
121
+ - For large files, prefer `downloadToPath` or `asWebStream`
122
+ - Automatic retries are applied for retryable errors (429, 5xx, network issues)
123
+ - Memory usage is kept stable for production workloads
124
+
125
+ ---
126
+
127
+ ## License
128
+
129
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,339 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ Office2PDF: () => Office2PDF,
34
+ Office2PDFError: () => Office2PDFError
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/client.ts
39
+ var import_undici = require("undici");
40
+ var import_formdata_node = require("formdata-node");
41
+ var import_file_from_path = require("formdata-node/file-from-path");
42
+ var import_promises2 = require("fs/promises");
43
+ var import_node_path = __toESM(require("path"), 1);
44
+
45
+ // src/errors.ts
46
+ var Office2PDFError = class extends Error {
47
+ code;
48
+ status;
49
+ requestId;
50
+ details;
51
+ constructor(args) {
52
+ super(args.message);
53
+ this.name = "Office2PDFError";
54
+ this.code = args.code;
55
+ this.status = args.status;
56
+ this.requestId = args.requestId;
57
+ this.details = args.details;
58
+ }
59
+ };
60
+
61
+ // src/utils.ts
62
+ var import_node_fs = require("fs");
63
+ var import_promises = require("stream/promises");
64
+ var import_node_stream = require("stream");
65
+ function sleep(ms) {
66
+ return new Promise((r) => setTimeout(r, ms));
67
+ }
68
+ function isRetryAbleStatus(status) {
69
+ return status === 408 || status === 429 || status >= 500 && status <= 599;
70
+ }
71
+ function getBackoffMs(attempt) {
72
+ const base = 300 * Math.pow(2, attempt);
73
+ const jitter = Math.floor(Math.random() * 150);
74
+ return base + jitter;
75
+ }
76
+ async function streamToFile(webStream, outPath) {
77
+ const nodeReadable = import_node_stream.Readable.fromWeb(webStream);
78
+ const ws = (0, import_node_fs.createWriteStream)(outPath);
79
+ await (0, import_promises.pipeline)(nodeReadable, ws);
80
+ }
81
+
82
+ // src/client.ts
83
+ var DEFAULT_BASE_URL = "https://api.office2pdf.app";
84
+ var DEFAULT_TIMEOUT_MS = 12e4;
85
+ var DEFAULT_MAX_RETRIES = 2;
86
+ function normalizeBaseUrl(baseUrl) {
87
+ const url = (baseUrl || DEFAULT_BASE_URL).trim();
88
+ return url.endsWith("/") ? url.slice(0, -1) : url;
89
+ }
90
+ function mapStatusToCode(status) {
91
+ const map = {
92
+ 401: "UNAUTHORIZED",
93
+ 403: "FORBIDDEN",
94
+ 404: "NOT_FOUND",
95
+ 429: "RATE_LIMITED",
96
+ 413: "QUOTA_EXCEEDED"
97
+ };
98
+ if (map[status]) return map[status];
99
+ if (status >= 400 && status < 500) return "INVALID_REQUEST";
100
+ if (status >= 500) return "SERVER_ERROR";
101
+ return "UNKNOWN";
102
+ }
103
+ async function safeReadJson(res) {
104
+ const ct = res.headers.get("content-type") ?? "";
105
+ if (!ct.includes("application/json")) return null;
106
+ try {
107
+ return await res.json();
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+ function getRequestId(res) {
113
+ return res.headers.get("x-request-id") ?? res.headers.get("cf-ray") ?? void 0;
114
+ }
115
+ function mergeAbortSignals(...signals) {
116
+ const active = signals.filter((s) => s != null);
117
+ if (active.length === 0) {
118
+ return { signal: void 0, cleanup: () => {
119
+ } };
120
+ }
121
+ if (active.length === 1) {
122
+ return { signal: active[0], cleanup: () => {
123
+ } };
124
+ }
125
+ const aborted = active.find((s) => s.aborted);
126
+ if (aborted) {
127
+ return { signal: aborted, cleanup: () => {
128
+ } };
129
+ }
130
+ const controller = new AbortController();
131
+ const onAbort = () => controller.abort();
132
+ active.forEach((s) => s.addEventListener("abort", onAbort));
133
+ return {
134
+ signal: controller.signal,
135
+ cleanup: () => {
136
+ active.forEach((s) => s.removeEventListener("abort", onAbort));
137
+ }
138
+ };
139
+ }
140
+ function asBodyInit(body) {
141
+ return body;
142
+ }
143
+ var Office2PDF = class {
144
+ apiKey;
145
+ baseUrl;
146
+ timeoutMs;
147
+ userAgent;
148
+ maxRetries;
149
+ constructor(opts) {
150
+ if (!opts?.apiKey?.trim()) {
151
+ throw new Error("Office2PDF: apiKey is required");
152
+ }
153
+ this.apiKey = opts.apiKey.trim();
154
+ this.baseUrl = normalizeBaseUrl(opts.baseUrl);
155
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
156
+ this.userAgent = opts.userAgent ?? "office2pdf-node";
157
+ this.maxRetries = Math.max(0, opts.maxRetries ?? DEFAULT_MAX_RETRIES);
158
+ }
159
+ /**
160
+ * Convert an Office document to PDF.
161
+ */
162
+ async convert(params) {
163
+ await this.validateParams(params);
164
+ const url = `${this.baseUrl}/api/pdf/preview`;
165
+ let lastError;
166
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
167
+ try {
168
+ return await this.sendConvertRequest(url, params);
169
+ } catch (err) {
170
+ const normalized = this.normalizeError(err);
171
+ lastError = normalized;
172
+ if (attempt < this.maxRetries && this.isRetryAble(normalized)) {
173
+ await sleep(getBackoffMs(attempt));
174
+ continue;
175
+ }
176
+ throw normalized;
177
+ }
178
+ }
179
+ throw lastError ?? new Office2PDFError({
180
+ message: "Unexpected conversion failure",
181
+ code: "UNKNOWN"
182
+ });
183
+ }
184
+ /* ------------------------------ Internals ------------------------------ */
185
+ async validateParams(params) {
186
+ if (!params.filePath?.trim()) {
187
+ throw new Office2PDFError({
188
+ message: "filePath is required",
189
+ code: "INVALID_REQUEST"
190
+ });
191
+ }
192
+ try {
193
+ await (0, import_promises2.access)(params.filePath, import_promises2.constants.R_OK);
194
+ } catch {
195
+ throw new Office2PDFError({
196
+ message: `File not found or not readable: ${params.filePath}`,
197
+ code: "INVALID_REQUEST"
198
+ });
199
+ }
200
+ if (params.asWebStream && params.downloadToPath) {
201
+ throw new Office2PDFError({
202
+ message: "Cannot use asWebStream with downloadToPath",
203
+ code: "INVALID_REQUEST"
204
+ });
205
+ }
206
+ if (params.downloadToPath) {
207
+ const dir = import_node_path.default.dirname(params.downloadToPath);
208
+ if (dir && dir !== ".") {
209
+ try {
210
+ await (0, import_promises2.access)(dir, import_promises2.constants.W_OK);
211
+ } catch {
212
+ throw new Office2PDFError({
213
+ message: `Download directory not writable: ${dir}`,
214
+ code: "INVALID_REQUEST"
215
+ });
216
+ }
217
+ }
218
+ }
219
+ }
220
+ async sendConvertRequest(url, params) {
221
+ const form = await this.buildFormData(params);
222
+ const timeoutCtrl = new AbortController();
223
+ const timeoutId = setTimeout(() => timeoutCtrl.abort(), this.timeoutMs);
224
+ const merged = mergeAbortSignals(params.signal, timeoutCtrl.signal);
225
+ try {
226
+ const res = await (0, import_undici.fetch)(url, {
227
+ method: "POST",
228
+ body: asBodyInit(form),
229
+ headers: {
230
+ "x-api-key": this.apiKey,
231
+ "User-Agent": this.userAgent
232
+ },
233
+ signal: merged.signal
234
+ });
235
+ return await this.handleResponse(res, params);
236
+ } finally {
237
+ clearTimeout(timeoutId);
238
+ merged.cleanup();
239
+ }
240
+ }
241
+ async buildFormData(params) {
242
+ const form = new import_formdata_node.FormData();
243
+ const file = await (0, import_file_from_path.fileFromPath)(params.filePath, params.fileName);
244
+ form.set("file", file);
245
+ form.set("output", params.output ?? "pdf");
246
+ if (params.password) {
247
+ form.set("password", params.password);
248
+ }
249
+ return form;
250
+ }
251
+ async handleResponse(res, params) {
252
+ const requestId = getRequestId(res);
253
+ if (!res.ok) {
254
+ const json = await safeReadJson(res);
255
+ const message = typeof json?.message === "string" ? json.message : typeof json?.error_description === "string" ? json.error_description : `Request failed with status ${res.status}`;
256
+ throw new Office2PDFError({
257
+ message,
258
+ code: mapStatusToCode(res.status),
259
+ status: res.status,
260
+ requestId,
261
+ details: json ?? void 0
262
+ });
263
+ }
264
+ const contentType = res.headers.get("content-type") ?? "application/pdf";
265
+ if (params.asWebStream) {
266
+ if (!res.body) {
267
+ throw new Office2PDFError({
268
+ message: "Empty response body",
269
+ code: "UNKNOWN",
270
+ requestId
271
+ });
272
+ }
273
+ return {
274
+ kind: "stream",
275
+ stream: res.body,
276
+ contentType,
277
+ requestId
278
+ };
279
+ }
280
+ if (params.downloadToPath) {
281
+ if (!res.body) {
282
+ throw new Office2PDFError({
283
+ message: "Empty response body",
284
+ code: "UNKNOWN",
285
+ requestId
286
+ });
287
+ }
288
+ await streamToFile(res.body, params.downloadToPath);
289
+ return {
290
+ kind: "downloaded",
291
+ path: params.downloadToPath,
292
+ contentType,
293
+ requestId
294
+ };
295
+ }
296
+ const buffer = Buffer.from(new Uint8Array(await res.arrayBuffer()));
297
+ return {
298
+ kind: "buffer",
299
+ buffer,
300
+ contentType,
301
+ requestId
302
+ };
303
+ }
304
+ normalizeError(err) {
305
+ if (err instanceof Office2PDFError) return err;
306
+ if (err instanceof DOMException && err.name === "AbortError") {
307
+ return new Office2PDFError({
308
+ message: "Request timed out",
309
+ code: "TIMEOUT"
310
+ });
311
+ }
312
+ if (err instanceof Error) {
313
+ return new Office2PDFError({
314
+ message: err.message,
315
+ code: "NETWORK_ERROR",
316
+ details: { name: err.name }
317
+ });
318
+ }
319
+ return new Office2PDFError({
320
+ message: "Unknown error",
321
+ code: "UNKNOWN",
322
+ details: err
323
+ });
324
+ }
325
+ isRetryAble(error) {
326
+ if (error.code === "TIMEOUT" || error.code === "NETWORK_ERROR") {
327
+ return true;
328
+ }
329
+ if (error.status && isRetryAbleStatus(error.status)) {
330
+ return true;
331
+ }
332
+ return false;
333
+ }
334
+ };
335
+ // Annotate the CommonJS export names for ESM import in node:
336
+ 0 && (module.exports = {
337
+ Office2PDF,
338
+ Office2PDFError
339
+ });
@@ -0,0 +1,90 @@
1
+ type Office2PDFClientOptions = {
2
+ apiKey: string;
3
+ baseUrl?: string;
4
+ timeoutMs?: number;
5
+ userAgent?: string;
6
+ maxRetries?: number;
7
+ };
8
+ type ConvertParams = {
9
+ filePath: string;
10
+ fileName?: string;
11
+ output?: "pdf";
12
+ password?: string;
13
+ signal?: AbortSignal;
14
+ downloadToPath?: string;
15
+ asWebStream?: boolean;
16
+ };
17
+ type ConvertResult = {
18
+ kind: "buffer";
19
+ buffer: Buffer;
20
+ contentType: string;
21
+ requestId?: string;
22
+ } | {
23
+ kind: "downloaded";
24
+ path: string;
25
+ contentType: string;
26
+ requestId?: string;
27
+ } | {
28
+ kind: "stream";
29
+ stream: ReadableStream<Uint8Array>;
30
+ contentType: string;
31
+ requestId?: string;
32
+ };
33
+
34
+ /**
35
+ * Official Office2PDF Node.js SDK client.
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * const client = new Office2PDF({ apiKey: "your-api-key" });
40
+ *
41
+ * // Get as buffer
42
+ * const result = await client.convert({ filePath: "./document.docx" });
43
+ * if (result.kind === "buffer") {
44
+ * fs.writeFileSync("output.pdf", result.buffer);
45
+ * }
46
+ *
47
+ * // Download directly to file
48
+ * await client.convert({
49
+ * filePath: "./document.docx",
50
+ * downloadToPath: "./output.pdf"
51
+ * });
52
+ * ```
53
+ * Converts Office documents (DOCX, XLSX, PPTX) to PDF.
54
+ * Authentication uses `x-api-key` header.
55
+ */
56
+ declare class Office2PDF {
57
+ private readonly apiKey;
58
+ private readonly baseUrl;
59
+ private readonly timeoutMs;
60
+ private readonly userAgent;
61
+ private readonly maxRetries;
62
+ constructor(opts: Office2PDFClientOptions);
63
+ /**
64
+ * Convert an Office document to PDF.
65
+ */
66
+ convert(params: ConvertParams): Promise<ConvertResult>;
67
+ private validateParams;
68
+ private sendConvertRequest;
69
+ private buildFormData;
70
+ private handleResponse;
71
+ private normalizeError;
72
+ private isRetryAble;
73
+ }
74
+
75
+ type Office2PDFErrorCode = "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "RATE_LIMITED" | "QUOTA_EXCEEDED" | "INVALID_REQUEST" | "SERVER_ERROR" | "NETWORK_ERROR" | "TIMEOUT" | "UNKNOWN";
76
+ declare class Office2PDFError extends Error {
77
+ readonly code: Office2PDFErrorCode;
78
+ readonly status?: number;
79
+ readonly requestId?: string;
80
+ readonly details?: unknown;
81
+ constructor(args: {
82
+ message: string;
83
+ code: Office2PDFErrorCode;
84
+ status?: number;
85
+ requestId?: string;
86
+ details?: unknown;
87
+ });
88
+ }
89
+
90
+ export { type ConvertParams, type ConvertResult, Office2PDF, type Office2PDFClientOptions, Office2PDFError };
@@ -0,0 +1,90 @@
1
+ type Office2PDFClientOptions = {
2
+ apiKey: string;
3
+ baseUrl?: string;
4
+ timeoutMs?: number;
5
+ userAgent?: string;
6
+ maxRetries?: number;
7
+ };
8
+ type ConvertParams = {
9
+ filePath: string;
10
+ fileName?: string;
11
+ output?: "pdf";
12
+ password?: string;
13
+ signal?: AbortSignal;
14
+ downloadToPath?: string;
15
+ asWebStream?: boolean;
16
+ };
17
+ type ConvertResult = {
18
+ kind: "buffer";
19
+ buffer: Buffer;
20
+ contentType: string;
21
+ requestId?: string;
22
+ } | {
23
+ kind: "downloaded";
24
+ path: string;
25
+ contentType: string;
26
+ requestId?: string;
27
+ } | {
28
+ kind: "stream";
29
+ stream: ReadableStream<Uint8Array>;
30
+ contentType: string;
31
+ requestId?: string;
32
+ };
33
+
34
+ /**
35
+ * Official Office2PDF Node.js SDK client.
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * const client = new Office2PDF({ apiKey: "your-api-key" });
40
+ *
41
+ * // Get as buffer
42
+ * const result = await client.convert({ filePath: "./document.docx" });
43
+ * if (result.kind === "buffer") {
44
+ * fs.writeFileSync("output.pdf", result.buffer);
45
+ * }
46
+ *
47
+ * // Download directly to file
48
+ * await client.convert({
49
+ * filePath: "./document.docx",
50
+ * downloadToPath: "./output.pdf"
51
+ * });
52
+ * ```
53
+ * Converts Office documents (DOCX, XLSX, PPTX) to PDF.
54
+ * Authentication uses `x-api-key` header.
55
+ */
56
+ declare class Office2PDF {
57
+ private readonly apiKey;
58
+ private readonly baseUrl;
59
+ private readonly timeoutMs;
60
+ private readonly userAgent;
61
+ private readonly maxRetries;
62
+ constructor(opts: Office2PDFClientOptions);
63
+ /**
64
+ * Convert an Office document to PDF.
65
+ */
66
+ convert(params: ConvertParams): Promise<ConvertResult>;
67
+ private validateParams;
68
+ private sendConvertRequest;
69
+ private buildFormData;
70
+ private handleResponse;
71
+ private normalizeError;
72
+ private isRetryAble;
73
+ }
74
+
75
+ type Office2PDFErrorCode = "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "RATE_LIMITED" | "QUOTA_EXCEEDED" | "INVALID_REQUEST" | "SERVER_ERROR" | "NETWORK_ERROR" | "TIMEOUT" | "UNKNOWN";
76
+ declare class Office2PDFError extends Error {
77
+ readonly code: Office2PDFErrorCode;
78
+ readonly status?: number;
79
+ readonly requestId?: string;
80
+ readonly details?: unknown;
81
+ constructor(args: {
82
+ message: string;
83
+ code: Office2PDFErrorCode;
84
+ status?: number;
85
+ requestId?: string;
86
+ details?: unknown;
87
+ });
88
+ }
89
+
90
+ export { type ConvertParams, type ConvertResult, Office2PDF, type Office2PDFClientOptions, Office2PDFError };
package/dist/index.js ADDED
@@ -0,0 +1,301 @@
1
+ // src/client.ts
2
+ import { fetch } from "undici";
3
+ import { FormData } from "formdata-node";
4
+ import { fileFromPath } from "formdata-node/file-from-path";
5
+ import { access, constants } from "fs/promises";
6
+ import path from "path";
7
+
8
+ // src/errors.ts
9
+ var Office2PDFError = class extends Error {
10
+ code;
11
+ status;
12
+ requestId;
13
+ details;
14
+ constructor(args) {
15
+ super(args.message);
16
+ this.name = "Office2PDFError";
17
+ this.code = args.code;
18
+ this.status = args.status;
19
+ this.requestId = args.requestId;
20
+ this.details = args.details;
21
+ }
22
+ };
23
+
24
+ // src/utils.ts
25
+ import { createWriteStream } from "fs";
26
+ import { pipeline } from "stream/promises";
27
+ import { Readable } from "stream";
28
+ function sleep(ms) {
29
+ return new Promise((r) => setTimeout(r, ms));
30
+ }
31
+ function isRetryAbleStatus(status) {
32
+ return status === 408 || status === 429 || status >= 500 && status <= 599;
33
+ }
34
+ function getBackoffMs(attempt) {
35
+ const base = 300 * Math.pow(2, attempt);
36
+ const jitter = Math.floor(Math.random() * 150);
37
+ return base + jitter;
38
+ }
39
+ async function streamToFile(webStream, outPath) {
40
+ const nodeReadable = Readable.fromWeb(webStream);
41
+ const ws = createWriteStream(outPath);
42
+ await pipeline(nodeReadable, ws);
43
+ }
44
+
45
+ // src/client.ts
46
+ var DEFAULT_BASE_URL = "https://api.office2pdf.app";
47
+ var DEFAULT_TIMEOUT_MS = 12e4;
48
+ var DEFAULT_MAX_RETRIES = 2;
49
+ function normalizeBaseUrl(baseUrl) {
50
+ const url = (baseUrl || DEFAULT_BASE_URL).trim();
51
+ return url.endsWith("/") ? url.slice(0, -1) : url;
52
+ }
53
+ function mapStatusToCode(status) {
54
+ const map = {
55
+ 401: "UNAUTHORIZED",
56
+ 403: "FORBIDDEN",
57
+ 404: "NOT_FOUND",
58
+ 429: "RATE_LIMITED",
59
+ 413: "QUOTA_EXCEEDED"
60
+ };
61
+ if (map[status]) return map[status];
62
+ if (status >= 400 && status < 500) return "INVALID_REQUEST";
63
+ if (status >= 500) return "SERVER_ERROR";
64
+ return "UNKNOWN";
65
+ }
66
+ async function safeReadJson(res) {
67
+ const ct = res.headers.get("content-type") ?? "";
68
+ if (!ct.includes("application/json")) return null;
69
+ try {
70
+ return await res.json();
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+ function getRequestId(res) {
76
+ return res.headers.get("x-request-id") ?? res.headers.get("cf-ray") ?? void 0;
77
+ }
78
+ function mergeAbortSignals(...signals) {
79
+ const active = signals.filter((s) => s != null);
80
+ if (active.length === 0) {
81
+ return { signal: void 0, cleanup: () => {
82
+ } };
83
+ }
84
+ if (active.length === 1) {
85
+ return { signal: active[0], cleanup: () => {
86
+ } };
87
+ }
88
+ const aborted = active.find((s) => s.aborted);
89
+ if (aborted) {
90
+ return { signal: aborted, cleanup: () => {
91
+ } };
92
+ }
93
+ const controller = new AbortController();
94
+ const onAbort = () => controller.abort();
95
+ active.forEach((s) => s.addEventListener("abort", onAbort));
96
+ return {
97
+ signal: controller.signal,
98
+ cleanup: () => {
99
+ active.forEach((s) => s.removeEventListener("abort", onAbort));
100
+ }
101
+ };
102
+ }
103
+ function asBodyInit(body) {
104
+ return body;
105
+ }
106
+ var Office2PDF = class {
107
+ apiKey;
108
+ baseUrl;
109
+ timeoutMs;
110
+ userAgent;
111
+ maxRetries;
112
+ constructor(opts) {
113
+ if (!opts?.apiKey?.trim()) {
114
+ throw new Error("Office2PDF: apiKey is required");
115
+ }
116
+ this.apiKey = opts.apiKey.trim();
117
+ this.baseUrl = normalizeBaseUrl(opts.baseUrl);
118
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
119
+ this.userAgent = opts.userAgent ?? "office2pdf-node";
120
+ this.maxRetries = Math.max(0, opts.maxRetries ?? DEFAULT_MAX_RETRIES);
121
+ }
122
+ /**
123
+ * Convert an Office document to PDF.
124
+ */
125
+ async convert(params) {
126
+ await this.validateParams(params);
127
+ const url = `${this.baseUrl}/api/pdf/preview`;
128
+ let lastError;
129
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
130
+ try {
131
+ return await this.sendConvertRequest(url, params);
132
+ } catch (err) {
133
+ const normalized = this.normalizeError(err);
134
+ lastError = normalized;
135
+ if (attempt < this.maxRetries && this.isRetryAble(normalized)) {
136
+ await sleep(getBackoffMs(attempt));
137
+ continue;
138
+ }
139
+ throw normalized;
140
+ }
141
+ }
142
+ throw lastError ?? new Office2PDFError({
143
+ message: "Unexpected conversion failure",
144
+ code: "UNKNOWN"
145
+ });
146
+ }
147
+ /* ------------------------------ Internals ------------------------------ */
148
+ async validateParams(params) {
149
+ if (!params.filePath?.trim()) {
150
+ throw new Office2PDFError({
151
+ message: "filePath is required",
152
+ code: "INVALID_REQUEST"
153
+ });
154
+ }
155
+ try {
156
+ await access(params.filePath, constants.R_OK);
157
+ } catch {
158
+ throw new Office2PDFError({
159
+ message: `File not found or not readable: ${params.filePath}`,
160
+ code: "INVALID_REQUEST"
161
+ });
162
+ }
163
+ if (params.asWebStream && params.downloadToPath) {
164
+ throw new Office2PDFError({
165
+ message: "Cannot use asWebStream with downloadToPath",
166
+ code: "INVALID_REQUEST"
167
+ });
168
+ }
169
+ if (params.downloadToPath) {
170
+ const dir = path.dirname(params.downloadToPath);
171
+ if (dir && dir !== ".") {
172
+ try {
173
+ await access(dir, constants.W_OK);
174
+ } catch {
175
+ throw new Office2PDFError({
176
+ message: `Download directory not writable: ${dir}`,
177
+ code: "INVALID_REQUEST"
178
+ });
179
+ }
180
+ }
181
+ }
182
+ }
183
+ async sendConvertRequest(url, params) {
184
+ const form = await this.buildFormData(params);
185
+ const timeoutCtrl = new AbortController();
186
+ const timeoutId = setTimeout(() => timeoutCtrl.abort(), this.timeoutMs);
187
+ const merged = mergeAbortSignals(params.signal, timeoutCtrl.signal);
188
+ try {
189
+ const res = await fetch(url, {
190
+ method: "POST",
191
+ body: asBodyInit(form),
192
+ headers: {
193
+ "x-api-key": this.apiKey,
194
+ "User-Agent": this.userAgent
195
+ },
196
+ signal: merged.signal
197
+ });
198
+ return await this.handleResponse(res, params);
199
+ } finally {
200
+ clearTimeout(timeoutId);
201
+ merged.cleanup();
202
+ }
203
+ }
204
+ async buildFormData(params) {
205
+ const form = new FormData();
206
+ const file = await fileFromPath(params.filePath, params.fileName);
207
+ form.set("file", file);
208
+ form.set("output", params.output ?? "pdf");
209
+ if (params.password) {
210
+ form.set("password", params.password);
211
+ }
212
+ return form;
213
+ }
214
+ async handleResponse(res, params) {
215
+ const requestId = getRequestId(res);
216
+ if (!res.ok) {
217
+ const json = await safeReadJson(res);
218
+ const message = typeof json?.message === "string" ? json.message : typeof json?.error_description === "string" ? json.error_description : `Request failed with status ${res.status}`;
219
+ throw new Office2PDFError({
220
+ message,
221
+ code: mapStatusToCode(res.status),
222
+ status: res.status,
223
+ requestId,
224
+ details: json ?? void 0
225
+ });
226
+ }
227
+ const contentType = res.headers.get("content-type") ?? "application/pdf";
228
+ if (params.asWebStream) {
229
+ if (!res.body) {
230
+ throw new Office2PDFError({
231
+ message: "Empty response body",
232
+ code: "UNKNOWN",
233
+ requestId
234
+ });
235
+ }
236
+ return {
237
+ kind: "stream",
238
+ stream: res.body,
239
+ contentType,
240
+ requestId
241
+ };
242
+ }
243
+ if (params.downloadToPath) {
244
+ if (!res.body) {
245
+ throw new Office2PDFError({
246
+ message: "Empty response body",
247
+ code: "UNKNOWN",
248
+ requestId
249
+ });
250
+ }
251
+ await streamToFile(res.body, params.downloadToPath);
252
+ return {
253
+ kind: "downloaded",
254
+ path: params.downloadToPath,
255
+ contentType,
256
+ requestId
257
+ };
258
+ }
259
+ const buffer = Buffer.from(new Uint8Array(await res.arrayBuffer()));
260
+ return {
261
+ kind: "buffer",
262
+ buffer,
263
+ contentType,
264
+ requestId
265
+ };
266
+ }
267
+ normalizeError(err) {
268
+ if (err instanceof Office2PDFError) return err;
269
+ if (err instanceof DOMException && err.name === "AbortError") {
270
+ return new Office2PDFError({
271
+ message: "Request timed out",
272
+ code: "TIMEOUT"
273
+ });
274
+ }
275
+ if (err instanceof Error) {
276
+ return new Office2PDFError({
277
+ message: err.message,
278
+ code: "NETWORK_ERROR",
279
+ details: { name: err.name }
280
+ });
281
+ }
282
+ return new Office2PDFError({
283
+ message: "Unknown error",
284
+ code: "UNKNOWN",
285
+ details: err
286
+ });
287
+ }
288
+ isRetryAble(error) {
289
+ if (error.code === "TIMEOUT" || error.code === "NETWORK_ERROR") {
290
+ return true;
291
+ }
292
+ if (error.status && isRetryAbleStatus(error.status)) {
293
+ return true;
294
+ }
295
+ return false;
296
+ }
297
+ };
298
+ export {
299
+ Office2PDF,
300
+ Office2PDFError
301
+ };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@office2pdf/node",
3
+ "version": "0.1.2",
4
+ "description": "Official Office2PDF Node.js SDK",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/politehq/office2pdf-sdks"
19
+ },
20
+ "homepage": "https://office2pdf.app",
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "keywords": [
27
+ "office2pdf",
28
+ "pdf",
29
+ "docx",
30
+ "xlsx",
31
+ "pptx",
32
+ "convert",
33
+ "sdk",
34
+ "api"
35
+ ],
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "license": "MIT",
40
+ "scripts": {
41
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
42
+ "typecheck": "tsc -p tsconfig.json --noEmit",
43
+ "test": "vitest run"
44
+ },
45
+ "dependencies": {
46
+ "formdata-node": "^6.0.3",
47
+ "undici": "^6.21.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^20.11.0",
51
+ "tsup": "^8.0.1",
52
+ "typescript": "^5.4.0",
53
+ "vitest": "^4.0.16"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "sideEffects": false
59
+ }