Skip to content

Commit 358cfd6

Browse files
jdrhynenickwinderclaude
authored
refactor: Remove client-side PDF parsing and add URL support (#8)
* fix: API correctness issues (SSRF protection, docs alignment) - Add allowUrlFetch option to client options (default: false) - Block automatic URL fetching by default for SSRF protection - Validate URL protocols (only http/https allowed) - Add helper method normalizeFileInput() to client - Update README.md with import statement in Quick Start - Add SSRF protection documentation section to README - Update and add tests for SSRF protection behavior Security: SSRF protection requires explicit opt-in for URL fetching. Users must set allowUrlFetch: true to enable client-side URL fetching. * Remove client-side PDF parsing and add URL support to methods - Remove getPdfPageCount, isValidPdf, and processRemoteFileInput functions - Remove allowUrlFetch client option (SSRF protection by design) - Most methods now accept FileInputWithUrl - URLs passed to server - Leverage API's native negative index support (-1 = last page) - sign() remains the only method requiring local files (API limitation) - Reduces bundle size by ~400 lines of PDF parsing code Co-Authored-By: Claude Opus 4.5 <[email protected]> * Fix CI: Replace non-null assertions with undefined checks ESLint forbids non-null assertions (!). Replaced array index accesses with explicit undefined checks that TypeScript can verify. Co-Authored-By: Claude Opus 4.5 <[email protected]> * Fix code quality issues from PR review - Replace unsafe type assertion (as unknown as T) with getRemoteUrl() helper - Validate HTML assets upfront before calling registerAssets - Add comprehensive tests for URL support in action file inputs - Remove unused import and fix linting issues All 266 tests pass. Fixes all issues identified in PR #8 review. Co-Authored-By: Claude Haiku 4.5 <[email protected]> --------- Co-authored-by: nickwinder <[email protected]> Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent f8c1f69 commit 358cfd6

File tree

13 files changed

+408
-938
lines changed

13 files changed

+408
-938
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,40 @@ The documentation for Nutrient DWS TypeScript Client is also available on [Conte
6060
## Quick Start
6161

6262
```typescript
63+
import { NutrientClient } from '@nutrient-sdk/dws-client-typescript';
64+
6365
const client = new NutrientClient({
6466
apiKey: 'nutr_sk_your_secret_key'
6567
});
6668
```
6769

70+
### Working with URLs
71+
72+
Most methods accept URLs directly. The URL is passed to the server, which fetches the content—this avoids SSRF vulnerabilities since the client never fetches URLs itself.
73+
74+
```typescript
75+
// Pass URL as a string
76+
const result = await client.convert('https://example.com/document.pdf', 'docx');
77+
78+
// Or as an object (useful for TypeScript type narrowing)
79+
const result = await client.convert({ type: 'url', url: 'https://example.com/document.pdf' }, 'docx');
80+
81+
// URLs also work with the workflow builder
82+
const result = await client.workflow()
83+
.addFilePart('https://example.com/document.pdf')
84+
.outputPdf()
85+
.execute();
86+
```
87+
88+
**Exception:** The `sign()` method only accepts local files (file paths, Buffers, streams) because the underlying API endpoint doesn't support URL inputs. For signing remote files, fetch the content first:
89+
90+
```typescript
91+
// Fetch and pass the bytes for signing
92+
const response = await fetch('https://example.com/document.pdf');
93+
const buffer = Buffer.from(await response.arrayBuffer());
94+
const result = await client.sign(buffer, { /* signature options */ });
95+
```
96+
6897
## Direct Methods
6998

7099
The client provides numerous methods for document processing:

package-lock.json

Lines changed: 0 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/__tests__/integration.test.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
samplePNG,
1919
TestDocumentGenerator,
2020
} from './helpers';
21-
import { getPdfPageCount, processFileInput } from '../inputs';
2221

2322
// Skip integration tests in CI/automated environments unless explicitly enabled with valid API key
2423
const shouldRunIntegrationTests = Boolean(process.env['NUTRIENT_API_KEY']);
@@ -236,13 +235,11 @@ describeIntegration('Integration Tests with Live API - Direct Methods', () => {
236235
describe('merge()', () => {
237236
it('should merge multiple PDF files', async () => {
238237
const result = await client.merge([samplePDF, samplePDF, samplePDF]);
239-
const normalizedPdf = await processFileInput(samplePDF);
240-
const pageCount = await getPdfPageCount(normalizedPdf);
241238
expect(result).toBeDefined();
242239
expect(result.buffer).toBeInstanceOf(Uint8Array);
243240
expect(result.mimeType).toBe('application/pdf');
244-
const normalizedResult = await processFileInput(result.buffer);
245-
await expect(getPdfPageCount(normalizedResult)).resolves.toBe(pageCount * 3);
241+
// Merged PDF should be larger than original
242+
expect(result.buffer.length).toBeGreaterThan(samplePDF.length);
246243
}, 60000);
247244
});
248245

src/__tests__/unit/client.test.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,6 @@ interface MockWorkflowWithOutputStage<T extends keyof OutputTypeMap | undefined
114114
const mockValidateFileInput = inputsModule.validateFileInput as jest.MockedFunction<
115115
typeof inputsModule.validateFileInput
116116
>;
117-
const mockIsValidPdf = inputsModule.isValidPdf as jest.MockedFunction<
118-
typeof inputsModule.isValidPdf
119-
>;
120-
const mockGetPdfPageCount = inputsModule.getPdfPageCount as jest.MockedFunction<
121-
typeof inputsModule.getPdfPageCount
122-
>;
123117
const mockSendRequest = httpModule.sendRequest as jest.MockedFunction<
124118
typeof httpModule.sendRequest
125119
>;
@@ -173,8 +167,6 @@ describe('NutrientClient', () => {
173167
beforeEach(() => {
174168
jest.clearAllMocks();
175169
mockValidateFileInput.mockReturnValue(true);
176-
mockIsValidPdf.mockResolvedValue(true);
177-
mockGetPdfPageCount.mockResolvedValue(10);
178170
mockSendRequest.mockResolvedValue({
179171
data: TestDocumentGenerator.generateSimplePdf() as never,
180172
status: 200,
@@ -239,6 +231,7 @@ describe('NutrientClient', () => {
239231
}),
240232
).toThrow('Base URL must be a string');
241233
});
234+
242235
});
243236

244237
describe('workflow()', () => {
@@ -1056,8 +1049,7 @@ describe('NutrientClient', () => {
10561049

10571050
await client.addPage(file, count, index);
10581051

1059-
// Mock getPdfPageCount to test index logic
1060-
// This is a simplified test since we can't easily mock the internal implementation
1052+
// Verify the workflow was called with the correct page ranges
10611053
expect(mockWorkflowInstance.addFilePart).toHaveBeenCalledWith(file, {
10621054
pages: { end: 1, start: 0 },
10631055
});
@@ -1169,18 +1161,19 @@ describe('NutrientClient', () => {
11691161

11701162
it('should delete specific pages from a document', async () => {
11711163
const file = 'test-file.pdf';
1172-
const pageIndices = [3, 1, 1];
1164+
const pageIndices = [3, 1, 1]; // Delete pages 1 and 3 (duplicates removed)
11731165

11741166
await client.deletePages(file, pageIndices);
11751167

1168+
// Should keep: [0-0], [2-2], [4 to end]
11761169
expect(mockWorkflowInstance.addFilePart).toHaveBeenCalledWith(file, {
1177-
pages: { end: 0, start: 0 },
1170+
pages: { start: 0, end: 0 },
11781171
});
11791172
expect(mockWorkflowInstance.addFilePart).toHaveBeenCalledWith(file, {
1180-
pages: { end: 2, start: 2 },
1173+
pages: { start: 2, end: 2 },
11811174
});
11821175
expect(mockWorkflowInstance.addFilePart).toHaveBeenCalledWith(file, {
1183-
pages: { end: 9, start: 4 },
1176+
pages: { start: 4, end: -1 }, // -1 means "to end of document"
11841177
});
11851178
expect(mockWorkflowInstance.addFilePart).toHaveBeenCalledTimes(3);
11861179
expect(mockWorkflowInstance.outputPdf).toHaveBeenCalled();

0 commit comments

Comments
 (0)