Skip to content

feat(documents): add PAPERLESS_EXTERNAL_URL for browser links (#411)#412

Merged
steilerDev merged 4 commits into
betafrom
feat/411-paperless-external-url
Mar 3, 2026
Merged

feat(documents): add PAPERLESS_EXTERNAL_URL for browser links (#411)#412
steilerDev merged 4 commits into
betafrom
feat/411-paperless-external-url

Conversation

@steilerDev
Copy link
Copy Markdown
Owner

Summary

  • Added PAPERLESS_EXTERNAL_URL optional env var for deployments where the server accesses Paperless-ngx via an internal URL but browsers need an external one
  • Server returns PAPERLESS_EXTERNAL_URL (when set) as paperlessUrl in GET /api/paperless/status, falling back to PAPERLESS_URL
  • Zero frontend changes — existing components automatically use the correct URL
  • Updated docs (setup guide, configuration reference), docker-compose.yml, and CLAUDE.md

Fixes #411

Test plan

  • 12 new unit/integration tests (9 config validation + 3 API response)
  • Pre-commit hook quality gates pass
  • Backward compatibility: no external URL = same behavior as before

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

claude added 2 commits March 3, 2026 18:08
…links

Add PAPERLESS_EXTERNAL_URL environment variable to support network setups where
the server accesses Paperless-ngx via an internal URL but browsers need an external
URL (e.g., PAPERLESS_URL=http://paperless:8000 internally,
PAPERLESS_EXTERNAL_URL=https://paperless.example.com for browsers).

Changes:
- Add paperlessExternalUrl to AppConfig interface
- Parse and validate PAPERLESS_EXTERNAL_URL in loadConfig()
- Update /api/paperless/status to return external URL when set, falling back to PAPERLESS_URL
- Update JSDoc for PaperlessStatusResponse.paperlessUrl
- Add PAPERLESS_EXTERNAL_URL to CLAUDE.md environment variables table
- Update docker-compose.yml with example configuration
- Update docs/src/guides/documents/setup.md with network examples
- Update docs/src/getting-started/configuration.md environment table

Co-Authored-By: Claude backend-developer (Haiku 4.5) <noreply@anthropic.com>
…AL_URL support

Tests cover configuration parsing and validation:
- PAPERLESS_EXTERNAL_URL not set → undefined
- Valid https:// and http:// URLs accepted
- Invalid schemes (file://, ftp://) rejected with clear error messages
- Invalid URL strings rejected with validation errors
- Empty string treated as undefined (consistent with other env vars)
- External URL can be set independently of internal URL
- External URL takes precedence over internal URL in API responses
- Backward compatibility: no external URL falls back to internal URL

Integration tests verify API response behavior:
- GET /api/paperless/status returns external URL when configured
- Status endpoint returns external URL even when Paperless unreachable
- Backward compatibility: internal URL used when no external URL configured

Fixes #411

Co-Authored-By: Claude qa-integration-tester (Haiku 4.5) <noreply@anthropic.com>
Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security-engineer] Security review of PR #412PAPERLESS_EXTERNAL_URL env var.

Summary

Reviewed the 9-file diff focused on SSRF prevention and env var validation. The implementation is clean and follows the established validation pattern for PAPERLESS_URL.

Security Analysis

SSRF Prevention — PASS

The critical security invariant is correctly maintained: PAPERLESS_EXTERNAL_URL is only used for browser-facing links; it never replaces paperlessUrl (the internal API proxy target) in any server-side fetch call.

  • requirePaperless() in paperless.ts returns fastify.config.paperlessUrl for all proxy endpoints — unchanged.
  • Only GET /api/paperless/status uses paperlessExternalUrl, and only in the response body for the client to use as an href.
  • The external URL never flows into paperlessService fetch calls.

URL Validation — PASS

PAPERLESS_EXTERNAL_URL receives identical scheme validation to PAPERLESS_URL:

  • new URL() parse with try/catch for malformed input
  • Allowlist: ['http:', 'https:'] — blocks file://, ftp://, javascript:, etc.
  • Empty string treated as undefined (correct behavior, no crash)
  • Validation errors accumulate in the shared errors[] array and throw together

Tests cover: https, http, file, ftp, invalid string, empty, standalone (no PAPERLESS_URL), and all-three-set scenarios.

Frontend URL Rendering — PASS

paperlessBaseUrl from the status response is used in two places:

  • DocumentDetailPanel.tsx:19 — ``${paperlessBaseUrl}/documents/${document.id}/details```
  • LinkedDocumentCard.tsx:85 — same pattern

Both render as href on <a target="_blank" rel="noopener noreferrer"> elements. Since the scheme is enforced server-side to http:/https: only, javascript: injection is not possible. The rel="noopener noreferrer attribute is already present.

The pre-existing open finding #17 from PR #400 (client-side javascript: URL guard) does not apply here — the URL originates from a server-validated config field, not from user-supplied data.

No New Attack Surface

  • No user-controlled input reaches this code path.
  • Config-time validation (startup fail-fast) prevents a misconfigured instance from serving bad links.
  • paperlessExternalUrl is not logged in the startup log (acceptable — it is a non-sensitive URL, but omitting it avoids any confusion with the internal URL).

Verdict

No security issues found. The implementation correctly separates browser-facing URLs from server-side proxy targets and applies the same validated scheme allowlisting already used for PAPERLESS_URL.

Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[product-architect]

Architecture Review: feat(documents): add PAPERLESS_EXTERNAL_URL for browser links (#411)

Verdict: Approve (posted as comment -- cannot approve own-repo PRs)

This is a clean, well-scoped enhancement. The approach of reusing the existing paperlessUrl field in PaperlessStatusResponse to always represent the browser-facing URL is the correct design -- it avoids adding a new field, avoids frontend changes, and maintains backward compatibility.

What was verified

Config plugin (server/src/plugins/config.ts)

  • paperlessExternalUrl added to AppConfig interface -- correct placement
  • URL validation mirrors PAPERLESS_URL validation exactly (scheme allowlist, URL parsing, error collection) -- consistent
  • Empty string treated as undefined via existing getValue() helper -- consistent with other env vars
  • paperlessEnabled remains gated on paperlessUrl + paperlessApiToken only -- PAPERLESS_EXTERNAL_URL alone does not enable the integration (AC 8 satisfied)

Route handler (server/src/routes/paperless.ts)

  • Line 126: paperlessExternalUrl ?? paperlessUrl ?? null -- correct fallback chain
  • Only the /status endpoint is affected. All proxy endpoints in requirePaperless() continue using paperlessUrl exclusively for server-to-server calls (AC 5 satisfied)

Shared types (shared/src/types/document.ts)

  • JSDoc updated to clarify paperlessUrl now represents the browser-facing URL -- good contract documentation
  • No new fields added, no breaking changes

Tests

  • 9 config validation tests covering: valid URLs, invalid schemes (file://, ftp://), invalid strings, empty string, independent of internal URL, combined with internal URL
  • 3 integration tests covering: external URL returned when reachable, external URL returned when unreachable, backward compatibility fallback
  • Test teardown properly cleans up process.env.PAPERLESS_EXTERNAL_URL

Documentation

  • CLAUDE.md env var table updated
  • docker-compose.yml example config updated with inline comments distinguishing internal vs external
  • Docs site: configuration reference and setup guide both updated with Docker network examples

Findings

  1. LOW -- Wiki not updated: The API-Contract.md wiki has an environment variables table (line 4389-4392) and a GET /api/paperless/status response shape (lines 4408-4434) that do not include PAPERLESS_EXTERNAL_URL or the paperlessUrl response field. The Architecture.md wiki Paperless-ngx section (lines 250-303) also lacks PAPERLESS_EXTERNAL_URL. These should be updated for wiki accuracy. The status response examples in the wiki have been missing the paperlessUrl field since EPIC-08 -- this PR is a good opportunity to fix both.

  2. INFO -- Config logging: The config plugin startup log (line 197) logs paperlessUrl but not paperlessExternalUrl. Not blocking, but logging it would help operators verify their configuration on startup.

Both findings are non-blocking. The implementation is architecturally sound, backward-compatible, well-tested, and properly documented in the source tree.

claude added 2 commits March 3, 2026 18:22
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@steilerDev steilerDev enabled auto-merge (squash) March 3, 2026 18:31
@steilerDev steilerDev merged commit 4d1a26a into beta Mar 3, 2026
8 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 3, 2026

🎉 This PR is included in version 1.12.0-beta.15 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 7, 2026

🎉 This PR is included in version 1.12.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@steilerDev steilerDev deleted the feat/411-paperless-external-url branch March 7, 2026 07:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants