Skip to content

Commit 7457a52

Browse files
HungKNguyennickwinderclaude
authored
Release/3.0.0 (#45)
* feat: add security changes * Update docs/MIGRATION.md Co-authored-by: Nick Winder <nfxdevelopment@gmail.com> * Update docs/MIGRATION.md Co-authored-by: Nick Winder <nfxdevelopment@gmail.com> * Rename FileInputWithUrl to FileInput, add UrlFileInput alias Replace the ambiguous FileInputWithUrl name with explicit type aliases: - LocalFileInput = Path | bytes | BinaryIO (unchanged) - UrlFileInput = str (new, explicit URL type) - FileInput = UrlFileInput | LocalFileInput (combined, replaces FileInputWithUrl) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix rotate and delete_pages with negative page indices - rotate(): change `start > 0` to `start != 0` so prefix pages are included when a negative start index is used (e.g. start=-3) - delete_pages(): rewrite keep-range algorithm to handle negative delete indices; previously all-negative inputs raised a spurious ValidationError instead of keeping the correct page range Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix ruff per-file-ignores and remove unused variable in test - pyproject.toml: add D102 to tests/* per-file-ignores so test methods are not required to have docstrings (the comment indicated this intent but the empty array had no effect) - test_client.py: remove unused `result` assignment in test_password_protect_pdf_with_permissions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Sort __all__ in __init__.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Nick Winder <nfxdevelopment@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 9099f93 commit 7457a52

File tree

11 files changed

+581
-573
lines changed

11 files changed

+581
-573
lines changed

CHANGELOG.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
## [3.0.0] - 2026-01-30
11+
12+
### Security
13+
14+
- **CRITICAL**: Removed client-side URL fetching to prevent SSRF vulnerabilities
15+
- URLs are now passed to the server for secure server-side fetching
16+
- Restricted `sign()` method to local files only (API limitation)
17+
18+
### Changed
19+
20+
- **BREAKING**: `sign()` only accepts local files (paths, bytes, file objects) - no URLs
21+
- **BREAKING**: Most methods now accept `FileInputWithUrl` - URLs passed to server
22+
- **BREAKING**: Removed client-side PDF parsing - leverage API's negative index support
23+
- Methods like `rotate()`, `split()`, `deletePages()` now support negative indices (-1 = last page)
24+
- All methods except `sign()` accept URLs that are passed securely to the server
25+
26+
### Removed
27+
28+
- **BREAKING**: Removed `process_remote_file_input()` from public API (security risk)
29+
- **BREAKING**: Removed `get_pdf_page_count()` from public API (client-side PDF parsing)
30+
- **BREAKING**: Removed `is_valid_pdf()` from public API (internal use only)
31+
- Removed ~200 lines of client-side PDF parsing code
32+
33+
### Added
34+
35+
- SSRF protection documentation in README
36+
- Migration guide (docs/MIGRATION.md)
37+
- Security best practices for handling remote files
38+
- Support for negative page indices in all page-based methods
39+
40+
## [2.0.0] - 2025-01-09
41+
42+
- Initial stable release with full API coverage
43+
- Async-first design with httpx and aiofiles
44+
- Comprehensive type hints and mypy strict mode
45+
- Workflow builder with staged pattern
46+
- Error hierarchy with typed exceptions

docs/MIGRATION.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Migration Guide: v2.x to v3.0
2+
3+
## Overview
4+
5+
Version 3.0.0 introduces SSRF protection and removes client-side PDF parsing.
6+
7+
## Key Changes
8+
9+
### 1. `sign()` No Longer Accepts URLs (API Limitation)
10+
11+
**Before (v2.x)**:
12+
```python
13+
result = await client.sign('https://example.com/document.pdf', {...})
14+
```
15+
16+
**After (v3.0)** - Fetch file first:
17+
```python
18+
import httpx
19+
20+
async with httpx.AsyncClient() as http:
21+
url = 'https://example.com/document.pdf'
22+
23+
# IMPORTANT: Validate URL
24+
if not url.startswith('https://trusted-domain.com/'):
25+
raise ValueError('URL not from trusted domain')
26+
27+
response = await http.get(url, timeout=10.0)
28+
response.raise_for_status()
29+
pdf_bytes = response.content
30+
31+
result = await client.sign(pdf_bytes, {...})
32+
```
33+
34+
### 2. Most Methods Now Accept URLs (Passed directly to DWS)
35+
36+
Good news! These methods now support URLs passed securely to the DWS:
37+
- `rotate()`, `split()`, `add_page()`, `duplicate_pages()`, `delete_pages()`
38+
- `set_page_labels()`, `set_metadata()`, `optimize()`
39+
- `flatten()`, `apply_instant_json()`, `apply_xfdf()`
40+
- All redaction methods
41+
- `convert()`, `ocr()`, `watermark_*()`, `extract_*()`, `merge()`, `password_protect()`
42+
43+
**Example**:
44+
```python
45+
# This now works!
46+
result = await client.rotate('https://example.com/doc.pdf', 90, pages={'start': 0, 'end': 5})
47+
```
48+
49+
### 3. Negative Page Indices Now Supported
50+
51+
Use negative indices for "from end" references:
52+
- `-1` = last page
53+
- `-2` = second-to-last page
54+
- etc.
55+
56+
**Examples**:
57+
```python
58+
# Rotate last 3 pages
59+
await client.rotate(pdf, 90, pages={'start': -3, 'end': -1})
60+
61+
# Delete first and last pages
62+
await client.delete_pages(pdf, [0, -1])
63+
64+
# Split: keep middle pages, excluding first and last
65+
await client.split(pdf, [{'start': 1, 'end': -2}])
66+
```
67+
68+
### 4. Removed from Public API
69+
70+
- `process_remote_file_input()` - No longer needed (URLs passed to server)
71+
- `get_pdf_page_count()` - Use negative indices instead
72+
- `is_valid_pdf()` - Let server validate (internal use only)
73+
74+
**Still Available:**
75+
- `is_remote_file_input()` - Helper to detect if input is a URL (still public)

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ nutrient_dws_scripts = [
1818

1919
[project]
2020
name = "nutrient-dws"
21-
version = "2.0.0"
21+
version = "3.0.0"
2222
description = "Python client library for Nutrient Document Web Services API"
2323
readme = "README.md"
2424
requires-python = ">=3.10"
@@ -112,7 +112,7 @@ ignore = [
112112
convention = "google"
113113

114114
[tool.ruff.lint.per-file-ignores]
115-
"tests/*" = [] # Don't require docstrings in tests, allow asserts
115+
"tests/*" = ["D102"] # Don't require docstrings in tests
116116

117117
[tool.mypy]
118118
python_version = "3.10"

src/nutrient_dws/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,28 @@
1212
ValidationError,
1313
)
1414
from nutrient_dws.inputs import (
15+
FileInput,
16+
LocalFileInput,
17+
UrlFileInput,
1518
is_remote_file_input,
1619
process_file_input,
17-
process_remote_file_input,
1820
validate_file_input,
1921
)
2022
from nutrient_dws.utils import get_library_version, get_user_agent
2123

2224
__all__ = [
2325
"APIError",
2426
"AuthenticationError",
27+
"FileInput",
28+
"LocalFileInput",
2529
"NetworkError",
2630
"NutrientClient",
2731
"NutrientError",
32+
"UrlFileInput",
2833
"ValidationError",
2934
"get_library_version",
3035
"get_user_agent",
3136
"is_remote_file_input",
3237
"process_file_input",
33-
"process_remote_file_input",
3438
"validate_file_input",
3539
]

src/nutrient_dws/builder/builder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def _register_asset(self, asset: FileInput) -> str:
8585
"""Register an asset in the workflow and return its key for use in actions.
8686
8787
Args:
88-
asset: The asset to register
88+
asset: The asset to register (must be local, not URL)
8989
9090
Returns:
9191
The asset key that can be used in BuildActions

0 commit comments

Comments
 (0)