Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 0 additions & 43 deletions .cfignore

This file was deleted.

49 changes: 24 additions & 25 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
":dependencyDashboard",
"helpers:pinGitHubActionDigests"
],
"labels": ["dependencies"],
"labels": [
"dependencies"
],
"prConcurrentLimit": 5,
"prHourlyLimit": 2,
"minimumReleaseAge": "14 days",
Expand All @@ -14,48 +16,45 @@
"osvVulnerabilityAlerts": true,
"vulnerabilityAlerts": {
"description": "Security fixes: raise immediately (no cooldown) and auto-merge once CI is green",
"labels": ["dependencies", "security"],
"labels": [
"dependencies",
"security"
],
"minimumReleaseAge": null,
"automerge": true,
"dependencyDashboardApproval": false
},
"packageRules": [
{
"description": "Auto-merge patch and minor updates once they clear the 14-day cooldown and CI is green",
"matchUpdateTypes": ["minor", "patch", "pin", "digest", "bump"],
"matchUpdateTypes": [
"minor",
"patch",
"pin",
"digest",
"bump"
],
"automerge": true
},
{
"description": "Never auto-merge major updates - review and merge them myself",
"matchUpdateTypes": ["major"],
"matchUpdateTypes": [
"major"
],
"automerge": false,
"dependencyDashboardApproval": true
},
{
"description": "Never auto-merge GitHub Actions digest repoints (same tag, new SHA) - the tj-actions-style compromise vector. Review the diff myself via the dashboard",
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["digest", "pinDigest"],
"matchManagers": [
"github-actions"
],
"matchUpdateTypes": [
"digest",
"pinDigest"
],
"automerge": false,
"dependencyDashboardApproval": true
},
{
"description": "Stay on the pdf.js v3 line - v4+ is an ESM rewrite that needs a code migration, and cdnjs has no matching SRI digest for it (causes the lookup-failure warning)",
"matchDatasources": ["cdnjs"],
"matchDepNames": ["pdf.js"],
"allowedVersions": "<4"
}
],
"customManagers": [
{
"customType": "regex",
"description": "Keep the cdnjs-hosted PDF.js and SheetJS libraries (and their SRI hashes) up to date across index.html, app.js (worker), and sw.js (cache list) so versions never drift",
"managerFilePatterns": ["/public/(index\\.html|app\\.js|sw\\.js)$/"],
"matchStrings": [
"cdnjs\\.cloudflare\\.com/ajax/libs/(?<depName>[^/]+?)/(?<currentValue>[^/]+?)/(?<asset>[^\"'\\s]+?)[\"'](?:[^>\\n]*?integrity=[\"'](?<currentDigest>sha\\d+-[^\"']+)[\"'])?"
],
"depNameTemplate": "{{{depName}}}",
"packageNameTemplate": "{{{depName}}}/{{{asset}}}",
"datasourceTemplate": "cdnjs"
}
]
}
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Check pinned cdnjs versions and SRI hashes
- name: Check vendored library files are present and consistently referenced
run: bash scripts/check-cdn-versions.sh

- name: Check app version is in sync across package.json, sw.js, index.html
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- Migrated hosting from Cloudflare Pages to Vercel. Response headers and
caching now live in `vercel.json` instead of `public/_headers`
- Self-hosted PDF.js and SheetJS under `public/vendor/` instead of loading
them from cdnjs
- Replaced Cloudflare Web Analytics with Vercel Analytics, served
same-origin

### Removed

- Cloudflare Turnstile: it had no server-side verification anywhere, so it
was pure client-side friction with no security value
- `public/_worker.js`, `public/_headers`, `public/_redirects`, `.cfignore`

## [1.5.2] - 2026-07-25

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2025-2026 NX1X
Copyright 2025-present NX1X (https://nx1xlab.dev)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
39 changes: 19 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ A project by [NX1X Lab](https://nx1xlab.dev).

## Overview

EasyConvert reads a PDF entirely in the browser using PDF.js, detects tabular data, and exports it as an `.xlsx` (via SheetJS) or `.csv` file. No file ever leaves the device: there is no server, no upload, and no data collection. It is deployed as a static site on Cloudflare Pages and installable as a Progressive Web App.
EasyConvert reads a PDF entirely in the browser using PDF.js, detects tabular data, and exports it as an `.xlsx` (via SheetJS) or `.csv` file. No file ever leaves the device: there is no server, no upload, and no data collection. It is deployed as a static site on Vercel and installable as a Progressive Web App.

## Features

Expand All @@ -31,44 +31,43 @@ EasyConvert reads a PDF entirely in the browser using PDF.js, detects tabular da

| Area | Choice |
| --- | --- |
| PDF parsing | PDF.js (cdnjs, Subresource Integrity pinned) |
| Spreadsheet export | SheetJS / xlsx (cdnjs, SRI pinned) |
| PDF parsing | PDF.js, self-hosted under `public/vendor/` |
| Spreadsheet export | SheetJS / xlsx, self-hosted under `public/vendor/` |
| Application code | Vanilla JavaScript, no framework |
| Styling | Plain CSS (Grid and Flexbox) |
| PWA | Service Worker + Web App Manifest |
| Bot protection | Cloudflare Turnstile |
| Hosting | Cloudflare Pages (static) |
| Analytics | Vercel Analytics (same-origin, no third-party script) |
| Hosting | Vercel (static) |

## Repository Layout

```
EasyConvert/
├── public/ # The deployable site (Cloudflare output directory)
├── public/ # The deployable site (Vercel output directory)
│ ├── index.html # Markup
│ ├── style.css # Styles
│ ├── app.js # Application logic
│ ├── sw.js # Service worker
│ ├── manifest.json # PWA manifest
│ ├── _headers # Cloudflare Pages response headers (CSP etc.)
│ ├── _redirects # Cloudflare Pages redirects
│ ├── _worker.js # Asset allowlist worker
│ ├── vendor/ # Self-hosted PDF.js and SheetJS
│ └── *.svg / *.png / *.ico
├── vercel.json # Response headers, caching
├── .github/
│ ├── renovate.json # Dependency automation
│ └── workflows/ # CodeQL, Gitleaks, dependency review, CI
├── scripts/ # Version + release helpers, CDN version check
├── scripts/ # Version + release helpers, vendored-file check
├── package.json
├── CHANGELOG.md
└── LICENSE
```

## Security

- **Content Security Policy** with no `'unsafe-inline'` (all JS and CSS are external files, no inline handlers).
- **Subresource Integrity** on the CDN-hosted PDF.js and SheetJS libraries.
- **Content Security Policy** with no `'unsafe-inline'` and no third-party origins at all (all JS and CSS are same-origin files, no inline handlers).
- **PDF.js and SheetJS are self-hosted**, not loaded from a CDN, so there is no third-party script origin in the trust chain.
- **Strict response headers**: HSTS (preload), `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, `Cross-Origin-Opener-Policy`, `Cross-Origin-Resource-Policy`.
- **Spreadsheet formula-injection guard** on CSV and XLSX export.
- **Supply chain**: committed lockfile, GitHub Actions pinned to commit SHAs, Renovate with a 14-day release cooldown, and CI that scans for secrets (Gitleaks), reviews dependencies, runs CodeQL, and verifies CDN versions stay in sync.
- **Supply chain**: committed lockfile, GitHub Actions pinned to commit SHAs, Renovate with a 14-day release cooldown, and CI that scans for secrets (Gitleaks), reviews dependencies, runs CodeQL, and verifies the vendored libraries are present and consistently referenced.

## Running Locally

Expand All @@ -84,19 +83,19 @@ npx serve public

Then open `http://localhost:8000`.

## Deployment (Cloudflare Pages)
## Deployment (Vercel)

The repository is deployed as a static site. To serve only the site directory and keep the rest of the repository private, set the build output directory to `public`.
The repository is deployed as a static site. To serve only the site directory and keep the rest of the repository private, the output directory is set to `public`.

1. In the Cloudflare dashboard, create a Pages project and connect this repository.
1. In the Vercel dashboard, create a project and connect this repository.
2. Build settings:
- **Framework preset**: None
- **Framework preset**: Other
- **Build command**: (leave empty)
- **Build output directory**: `public`
- **Output directory**: `public`
- **Root directory**: `/`
3. Deploy. Only the contents of `public/` are published; everything else in the repository (docs, CI config, package files) is never served.

`public/_headers` applies the security headers and `public/_redirects` handles routing. No environment variables are required.
`vercel.json` applies the security headers and caching rules. No environment variables are required.

## Usage

Expand All @@ -115,7 +114,7 @@ This project follows [Semantic Versioning](https://semver.org/). See [CHANGELOG.

- [PDF.js](https://mozilla.github.io/pdf.js/) - PDF parsing
- [SheetJS](https://sheetjs.com/) - spreadsheet generation
- [Cloudflare Pages](https://pages.cloudflare.com/) - hosting
- [Vercel](https://vercel.com/) - hosting

## License

Expand Down
80 changes: 0 additions & 80 deletions public/_headers

This file was deleted.

3 changes: 0 additions & 3 deletions public/_redirects

This file was deleted.

35 changes: 0 additions & 35 deletions public/_worker.js

This file was deleted.

Loading