MistLiquit commited on
Commit
1ec6698
·
verified ·
1 Parent(s): 3950c34

Deploy pdf-api

Browse files
Files changed (10) hide show
  1. .dockerignore +4 -0
  2. .env.example +12 -0
  3. .gitignore +4 -0
  4. Dockerfile +12 -0
  5. README.md +120 -7
  6. package-lock.json +1000 -0
  7. package.json +15 -0
  8. render.yaml +12 -0
  9. src/index.js +154 -0
  10. src/templates/invoice.html +78 -0
.dockerignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ node_modules
2
+ .git
3
+ .env
4
+ *.log
.env.example ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Authentication (set at least one before any public deploy) ---
2
+ # RapidAPI sends this secret on every proxied request via the header
3
+ # X-RapidAPI-Proxy-Secret. Set it so ONLY RapidAPI's gateway can reach you.
4
+ RAPIDAPI_PROXY_SECRET=
5
+
6
+ # Simple key for direct/local access. Clients send header: x-api-key: <value>
7
+ API_KEY=
8
+
9
+ # If BOTH are empty the API is OPEN (local testing only) and logs a warning.
10
+
11
+ # Port the server listens on. Cloud Run / Render inject this automatically.
12
+ PORT=8080
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ node_modules/
2
+ .env
3
+ *.log
4
+ samples/
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Base image ships Chromium matching Playwright 1.60.0 (keep both in sync).
2
+ FROM mcr.microsoft.com/playwright:v1.60.0-jammy
3
+ WORKDIR /app
4
+ COPY package.json ./
5
+ RUN npm install --omit=dev
6
+ COPY . .
7
+ # HF Spaces runs the container as UID 1000 and routes traffic to app_port (7860);
8
+ # HOME must be writable for Chromium. (On Render, PORT is injected and overrides this.)
9
+ ENV HOME=/tmp
10
+ ENV PORT=7860
11
+ EXPOSE 7860
12
+ CMD ["npm", "start"]
README.md CHANGED
@@ -1,12 +1,125 @@
1
  ---
2
- title: Pdf Api
3
- emoji: 👀
4
- colorFrom: yellow
5
- colorTo: yellow
6
  sdk: docker
 
7
  pinned: false
8
- license: other
9
- short_description: Developer API to generate invoices, PDFs, and screenshots fr
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: PDF API
3
+ emoji: 📄
4
+ colorFrom: indigo
5
+ colorTo: blue
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
 
 
9
  ---
10
 
11
+ # PDF API
12
+
13
+ API sederhana untuk mengubah **HTML atau URL menjadi PDF**, ditenagai Express + Playwright (Chromium).
14
+ Tujuan: dijual sebagai API di RapidAPI dengan biaya infra minimal.
15
+
16
+ ## Endpoint
17
+
18
+ | Method | Path | Auth | Keterangan |
19
+ | ------ | ------------- | ------ | -------------------------------------------- |
20
+ | GET | `/health` | publik | Cek status (untuk health check) |
21
+ | POST | `/pdf` | ya | PDF dari `html` atau `url` |
22
+ | POST | `/screenshot` | ya | Tangkapan layar (PNG/JPEG) dari `html`/`url` |
23
+
24
+ Auth: kirim header `X-RapidAPI-Proxy-Secret` (lewat RapidAPI) atau `x-api-key`.
25
+
26
+ Body `POST /pdf` (JSON):
27
+
28
+ ```json
29
+ { "html": "<h1>Halo</h1>", "format": "A4", "landscape": false }
30
+ ```
31
+
32
+ Body `POST /screenshot` (JSON):
33
+
34
+ ```json
35
+ { "url": "https://example.com", "fullPage": true, "type": "png" }
36
+ ```
37
+
38
+ Pakai **template + data** (Handlebars) untuk dokumen seperti invoice — pembeli cukup kirim data:
39
+
40
+ ```json
41
+ {
42
+ "template": "<h1>Invoice {{number}}</h1>{{#each items}}<p>{{name}}: {{price}}</p>{{/each}}<b>Total: {{total}}</b>",
43
+ "data": { "number": "INV-001", "items": [{ "name": "Design", "price": "$50" }], "total": "$50" }
44
+ }
45
+ ```
46
+
47
+ Pilih salah satu dari `html`, `url`, atau `template` (+ `data`) — berlaku untuk `/pdf` dan `/screenshot`.
48
+
49
+ Atau pakai **template bawaan** (`templateName`) — pembeli cukup kirim `data`, tanpa menyusun HTML. Tersedia: `invoice`.
50
+
51
+ ```json
52
+ {
53
+ "templateName": "invoice",
54
+ "data": {
55
+ "company": { "name": "Acme Co", "email": "billing@acme.co" },
56
+ "client": { "name": "John Doe" },
57
+ "invoice": { "number": "INV-001", "date": "2026-06-01", "due": "2026-06-15" },
58
+ "items": [{ "description": "Design work", "quantity": 2, "price": "$25", "amount": "$50" }],
59
+ "subtotal": "$50", "tax": "$5", "total": "$55",
60
+ "notes": "Thank you!"
61
+ }
62
+ }
63
+ ```
64
+
65
+ Response: `application/pdf`, atau `image/png` | `image/jpeg` (binary).
66
+
67
+ ## Jalan lokal
68
+
69
+ ```bash
70
+ npm install
71
+ npx playwright install chromium # unduh browser sekali saja
72
+ npm start # default http://localhost:8080
73
+ ```
74
+
75
+ Tes cepat (hasil tersimpan ke out.pdf):
76
+
77
+ ```bash
78
+ curl -X POST http://localhost:8080/pdf \
79
+ -H "Content-Type: application/json" \
80
+ -d "{\"html\":\"<h1>Halo PDF</h1>\"}" \
81
+ --output out.pdf
82
+ ```
83
+
84
+ ## Deploy gratis (pilih salah satu)
85
+
86
+ **Render / Railway / Fly.io / Google Cloud Run** — semuanya membaca `Dockerfile`:
87
+
88
+ 1. Push repo ini ke GitHub.
89
+ 2. Buat service baru dari repo, pilih environment **Docker**.
90
+ 3. Set environment variable `API_KEY` dengan nilai rahasia.
91
+
92
+ Cloud Run cocok karena scale-to-zero (tidak ada tagihan saat sepi) dan free tier besar.
93
+
94
+ ## Integrasi RapidAPI
95
+
96
+ 1. Deploy dulu, catat URL publik backend (mis. `https://xxx.onrender.com`).
97
+ 2. Di RapidAPI: **Add New API** → arahkan Base URL ke backend Anda.
98
+ 3. Aktifkan opsi RapidAPI yang mengirim header rahasia `X-RapidAPI-Proxy-Secret`,
99
+ lalu ubah autentikasi backend untuk memverifikasi header itu (lihat catatan keamanan).
100
+ 4. Tetapkan paket harga (mis. freemium + bayar per request).
101
+
102
+ ## Keamanan
103
+
104
+ Sudah terpasang:
105
+
106
+ - **Autentikasi:** set `RAPIDAPI_PROXY_SECRET` (diverifikasi dari header
107
+ `X-RapidAPI-Proxy-Secret`) agar hanya gateway RapidAPI yang bisa mengakses
108
+ backend, dan/atau `API_KEY` (header `x-api-key`) untuk akses langsung.
109
+ Perbandingan rahasia memakai constant-time. Jika keduanya kosong, API TERBUKA
110
+ (hanya untuk tes lokal) dan server menampilkan peringatan saat start.
111
+ - **Proteksi SSRF:** setiap request jaringan yang dibuat halaman (navigasi awal,
112
+ redirect, dan sub-resource) divalidasi; alamat privat/loopback/link-local
113
+ (termasuk metadata cloud `169.254.169.254`) serta skema non-http(s) seperti
114
+ `file:` diblokir.
115
+
116
+ Disarankan sebelum skala besar:
117
+
118
+ - **Timeout render** agar tidak mudah di-abuse (body HTML sudah dibatasi 2 MB).
119
+ - **DNS rebinding:** validasi saat ini mengecek IP hasil resolve; untuk hardening
120
+ penuh, pin IP yang sudah divalidasi saat fetch.
121
+
122
+ ## Langkah berikutnya
123
+
124
+ - Tambah opsi margin, ukuran halaman kustom, dan header/footer untuk PDF.
125
+ - Tambah timeout render dan rate limiting agar tahan abuse.
package-lock.json ADDED
@@ -0,0 +1,1000 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "pdf-api",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "pdf-api",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "express": "4.22.2",
12
+ "handlebars": "4.7.9",
13
+ "playwright": "1.60.0"
14
+ }
15
+ },
16
+ "node_modules/accepts": {
17
+ "version": "1.3.8",
18
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
19
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "mime-types": "~2.1.34",
23
+ "negotiator": "0.6.3"
24
+ },
25
+ "engines": {
26
+ "node": ">= 0.6"
27
+ }
28
+ },
29
+ "node_modules/array-flatten": {
30
+ "version": "1.1.1",
31
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
32
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
33
+ "license": "MIT"
34
+ },
35
+ "node_modules/body-parser": {
36
+ "version": "1.20.5",
37
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
38
+ "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
39
+ "license": "MIT",
40
+ "dependencies": {
41
+ "bytes": "~3.1.2",
42
+ "content-type": "~1.0.5",
43
+ "debug": "2.6.9",
44
+ "depd": "2.0.0",
45
+ "destroy": "~1.2.0",
46
+ "http-errors": "~2.0.1",
47
+ "iconv-lite": "~0.4.24",
48
+ "on-finished": "~2.4.1",
49
+ "qs": "~6.15.1",
50
+ "raw-body": "~2.5.3",
51
+ "type-is": "~1.6.18",
52
+ "unpipe": "~1.0.0"
53
+ },
54
+ "engines": {
55
+ "node": ">= 0.8",
56
+ "npm": "1.2.8000 || >= 1.4.16"
57
+ }
58
+ },
59
+ "node_modules/body-parser/node_modules/http-errors": {
60
+ "version": "2.0.1",
61
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
62
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
63
+ "license": "MIT",
64
+ "dependencies": {
65
+ "depd": "~2.0.0",
66
+ "inherits": "~2.0.4",
67
+ "setprototypeof": "~1.2.0",
68
+ "statuses": "~2.0.2",
69
+ "toidentifier": "~1.0.1"
70
+ },
71
+ "engines": {
72
+ "node": ">= 0.8"
73
+ },
74
+ "funding": {
75
+ "type": "opencollective",
76
+ "url": "https://opencollective.com/express"
77
+ }
78
+ },
79
+ "node_modules/body-parser/node_modules/statuses": {
80
+ "version": "2.0.2",
81
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
82
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
83
+ "license": "MIT",
84
+ "engines": {
85
+ "node": ">= 0.8"
86
+ }
87
+ },
88
+ "node_modules/bytes": {
89
+ "version": "3.1.2",
90
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
91
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
92
+ "license": "MIT",
93
+ "engines": {
94
+ "node": ">= 0.8"
95
+ }
96
+ },
97
+ "node_modules/call-bind-apply-helpers": {
98
+ "version": "1.0.2",
99
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
100
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
101
+ "license": "MIT",
102
+ "dependencies": {
103
+ "es-errors": "^1.3.0",
104
+ "function-bind": "^1.1.2"
105
+ },
106
+ "engines": {
107
+ "node": ">= 0.4"
108
+ }
109
+ },
110
+ "node_modules/call-bound": {
111
+ "version": "1.0.4",
112
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
113
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
114
+ "license": "MIT",
115
+ "dependencies": {
116
+ "call-bind-apply-helpers": "^1.0.2",
117
+ "get-intrinsic": "^1.3.0"
118
+ },
119
+ "engines": {
120
+ "node": ">= 0.4"
121
+ },
122
+ "funding": {
123
+ "url": "https://github.com/sponsors/ljharb"
124
+ }
125
+ },
126
+ "node_modules/content-disposition": {
127
+ "version": "0.5.4",
128
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
129
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
130
+ "license": "MIT",
131
+ "dependencies": {
132
+ "safe-buffer": "5.2.1"
133
+ },
134
+ "engines": {
135
+ "node": ">= 0.6"
136
+ }
137
+ },
138
+ "node_modules/content-type": {
139
+ "version": "1.0.5",
140
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
141
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
142
+ "license": "MIT",
143
+ "engines": {
144
+ "node": ">= 0.6"
145
+ }
146
+ },
147
+ "node_modules/cookie": {
148
+ "version": "0.7.1",
149
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
150
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
151
+ "license": "MIT",
152
+ "engines": {
153
+ "node": ">= 0.6"
154
+ }
155
+ },
156
+ "node_modules/cookie-signature": {
157
+ "version": "1.0.6",
158
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
159
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
160
+ "license": "MIT"
161
+ },
162
+ "node_modules/debug": {
163
+ "version": "2.6.9",
164
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
165
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
166
+ "license": "MIT",
167
+ "dependencies": {
168
+ "ms": "2.0.0"
169
+ }
170
+ },
171
+ "node_modules/depd": {
172
+ "version": "2.0.0",
173
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
174
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
175
+ "license": "MIT",
176
+ "engines": {
177
+ "node": ">= 0.8"
178
+ }
179
+ },
180
+ "node_modules/destroy": {
181
+ "version": "1.2.0",
182
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
183
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
184
+ "license": "MIT",
185
+ "engines": {
186
+ "node": ">= 0.8",
187
+ "npm": "1.2.8000 || >= 1.4.16"
188
+ }
189
+ },
190
+ "node_modules/dunder-proto": {
191
+ "version": "1.0.1",
192
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
193
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
194
+ "license": "MIT",
195
+ "dependencies": {
196
+ "call-bind-apply-helpers": "^1.0.1",
197
+ "es-errors": "^1.3.0",
198
+ "gopd": "^1.2.0"
199
+ },
200
+ "engines": {
201
+ "node": ">= 0.4"
202
+ }
203
+ },
204
+ "node_modules/ee-first": {
205
+ "version": "1.1.1",
206
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
207
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
208
+ "license": "MIT"
209
+ },
210
+ "node_modules/encodeurl": {
211
+ "version": "2.0.0",
212
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
213
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
214
+ "license": "MIT",
215
+ "engines": {
216
+ "node": ">= 0.8"
217
+ }
218
+ },
219
+ "node_modules/es-define-property": {
220
+ "version": "1.0.1",
221
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
222
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
223
+ "license": "MIT",
224
+ "engines": {
225
+ "node": ">= 0.4"
226
+ }
227
+ },
228
+ "node_modules/es-errors": {
229
+ "version": "1.3.0",
230
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
231
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
232
+ "license": "MIT",
233
+ "engines": {
234
+ "node": ">= 0.4"
235
+ }
236
+ },
237
+ "node_modules/es-object-atoms": {
238
+ "version": "1.1.2",
239
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
240
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
241
+ "license": "MIT",
242
+ "dependencies": {
243
+ "es-errors": "^1.3.0"
244
+ },
245
+ "engines": {
246
+ "node": ">= 0.4"
247
+ }
248
+ },
249
+ "node_modules/escape-html": {
250
+ "version": "1.0.3",
251
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
252
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
253
+ "license": "MIT"
254
+ },
255
+ "node_modules/etag": {
256
+ "version": "1.8.1",
257
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
258
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
259
+ "license": "MIT",
260
+ "engines": {
261
+ "node": ">= 0.6"
262
+ }
263
+ },
264
+ "node_modules/express": {
265
+ "version": "4.22.2",
266
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
267
+ "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
268
+ "license": "MIT",
269
+ "dependencies": {
270
+ "accepts": "~1.3.8",
271
+ "array-flatten": "1.1.1",
272
+ "body-parser": "~1.20.5",
273
+ "content-disposition": "~0.5.4",
274
+ "content-type": "~1.0.4",
275
+ "cookie": "~0.7.1",
276
+ "cookie-signature": "~1.0.6",
277
+ "debug": "2.6.9",
278
+ "depd": "2.0.0",
279
+ "encodeurl": "~2.0.0",
280
+ "escape-html": "~1.0.3",
281
+ "etag": "~1.8.1",
282
+ "finalhandler": "~1.3.1",
283
+ "fresh": "~0.5.2",
284
+ "http-errors": "~2.0.0",
285
+ "merge-descriptors": "1.0.3",
286
+ "methods": "~1.1.2",
287
+ "on-finished": "~2.4.1",
288
+ "parseurl": "~1.3.3",
289
+ "path-to-regexp": "~0.1.12",
290
+ "proxy-addr": "~2.0.7",
291
+ "qs": "~6.15.1",
292
+ "range-parser": "~1.2.1",
293
+ "safe-buffer": "5.2.1",
294
+ "send": "~0.19.0",
295
+ "serve-static": "~1.16.2",
296
+ "setprototypeof": "1.2.0",
297
+ "statuses": "~2.0.1",
298
+ "type-is": "~1.6.18",
299
+ "utils-merge": "1.0.1",
300
+ "vary": "~1.1.2"
301
+ },
302
+ "engines": {
303
+ "node": ">= 0.10.0"
304
+ },
305
+ "funding": {
306
+ "type": "opencollective",
307
+ "url": "https://opencollective.com/express"
308
+ }
309
+ },
310
+ "node_modules/finalhandler": {
311
+ "version": "1.3.1",
312
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
313
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
314
+ "license": "MIT",
315
+ "dependencies": {
316
+ "debug": "2.6.9",
317
+ "encodeurl": "~2.0.0",
318
+ "escape-html": "~1.0.3",
319
+ "on-finished": "2.4.1",
320
+ "parseurl": "~1.3.3",
321
+ "statuses": "2.0.1",
322
+ "unpipe": "~1.0.0"
323
+ },
324
+ "engines": {
325
+ "node": ">= 0.8"
326
+ }
327
+ },
328
+ "node_modules/forwarded": {
329
+ "version": "0.2.0",
330
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
331
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
332
+ "license": "MIT",
333
+ "engines": {
334
+ "node": ">= 0.6"
335
+ }
336
+ },
337
+ "node_modules/fresh": {
338
+ "version": "0.5.2",
339
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
340
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
341
+ "license": "MIT",
342
+ "engines": {
343
+ "node": ">= 0.6"
344
+ }
345
+ },
346
+ "node_modules/fsevents": {
347
+ "version": "2.3.2",
348
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
349
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
350
+ "hasInstallScript": true,
351
+ "license": "MIT",
352
+ "optional": true,
353
+ "os": [
354
+ "darwin"
355
+ ],
356
+ "engines": {
357
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
358
+ }
359
+ },
360
+ "node_modules/function-bind": {
361
+ "version": "1.1.2",
362
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
363
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
364
+ "license": "MIT",
365
+ "funding": {
366
+ "url": "https://github.com/sponsors/ljharb"
367
+ }
368
+ },
369
+ "node_modules/get-intrinsic": {
370
+ "version": "1.3.0",
371
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
372
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
373
+ "license": "MIT",
374
+ "dependencies": {
375
+ "call-bind-apply-helpers": "^1.0.2",
376
+ "es-define-property": "^1.0.1",
377
+ "es-errors": "^1.3.0",
378
+ "es-object-atoms": "^1.1.1",
379
+ "function-bind": "^1.1.2",
380
+ "get-proto": "^1.0.1",
381
+ "gopd": "^1.2.0",
382
+ "has-symbols": "^1.1.0",
383
+ "hasown": "^2.0.2",
384
+ "math-intrinsics": "^1.1.0"
385
+ },
386
+ "engines": {
387
+ "node": ">= 0.4"
388
+ },
389
+ "funding": {
390
+ "url": "https://github.com/sponsors/ljharb"
391
+ }
392
+ },
393
+ "node_modules/get-proto": {
394
+ "version": "1.0.1",
395
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
396
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
397
+ "license": "MIT",
398
+ "dependencies": {
399
+ "dunder-proto": "^1.0.1",
400
+ "es-object-atoms": "^1.0.0"
401
+ },
402
+ "engines": {
403
+ "node": ">= 0.4"
404
+ }
405
+ },
406
+ "node_modules/gopd": {
407
+ "version": "1.2.0",
408
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
409
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
410
+ "license": "MIT",
411
+ "engines": {
412
+ "node": ">= 0.4"
413
+ },
414
+ "funding": {
415
+ "url": "https://github.com/sponsors/ljharb"
416
+ }
417
+ },
418
+ "node_modules/handlebars": {
419
+ "version": "4.7.9",
420
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
421
+ "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
422
+ "license": "MIT",
423
+ "dependencies": {
424
+ "minimist": "^1.2.5",
425
+ "neo-async": "^2.6.2",
426
+ "source-map": "^0.6.1",
427
+ "wordwrap": "^1.0.0"
428
+ },
429
+ "bin": {
430
+ "handlebars": "bin/handlebars"
431
+ },
432
+ "engines": {
433
+ "node": ">=0.4.7"
434
+ },
435
+ "optionalDependencies": {
436
+ "uglify-js": "^3.1.4"
437
+ }
438
+ },
439
+ "node_modules/has-symbols": {
440
+ "version": "1.1.0",
441
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
442
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
443
+ "license": "MIT",
444
+ "engines": {
445
+ "node": ">= 0.4"
446
+ },
447
+ "funding": {
448
+ "url": "https://github.com/sponsors/ljharb"
449
+ }
450
+ },
451
+ "node_modules/hasown": {
452
+ "version": "2.0.4",
453
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
454
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
455
+ "license": "MIT",
456
+ "dependencies": {
457
+ "function-bind": "^1.1.2"
458
+ },
459
+ "engines": {
460
+ "node": ">= 0.4"
461
+ }
462
+ },
463
+ "node_modules/http-errors": {
464
+ "version": "2.0.0",
465
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
466
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
467
+ "license": "MIT",
468
+ "dependencies": {
469
+ "depd": "2.0.0",
470
+ "inherits": "2.0.4",
471
+ "setprototypeof": "1.2.0",
472
+ "statuses": "2.0.1",
473
+ "toidentifier": "1.0.1"
474
+ },
475
+ "engines": {
476
+ "node": ">= 0.8"
477
+ }
478
+ },
479
+ "node_modules/iconv-lite": {
480
+ "version": "0.4.24",
481
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
482
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
483
+ "license": "MIT",
484
+ "dependencies": {
485
+ "safer-buffer": ">= 2.1.2 < 3"
486
+ },
487
+ "engines": {
488
+ "node": ">=0.10.0"
489
+ }
490
+ },
491
+ "node_modules/inherits": {
492
+ "version": "2.0.4",
493
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
494
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
495
+ "license": "ISC"
496
+ },
497
+ "node_modules/ipaddr.js": {
498
+ "version": "1.9.1",
499
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
500
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
501
+ "license": "MIT",
502
+ "engines": {
503
+ "node": ">= 0.10"
504
+ }
505
+ },
506
+ "node_modules/math-intrinsics": {
507
+ "version": "1.1.0",
508
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
509
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
510
+ "license": "MIT",
511
+ "engines": {
512
+ "node": ">= 0.4"
513
+ }
514
+ },
515
+ "node_modules/media-typer": {
516
+ "version": "0.3.0",
517
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
518
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
519
+ "license": "MIT",
520
+ "engines": {
521
+ "node": ">= 0.6"
522
+ }
523
+ },
524
+ "node_modules/merge-descriptors": {
525
+ "version": "1.0.3",
526
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
527
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
528
+ "license": "MIT",
529
+ "funding": {
530
+ "url": "https://github.com/sponsors/sindresorhus"
531
+ }
532
+ },
533
+ "node_modules/methods": {
534
+ "version": "1.1.2",
535
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
536
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
537
+ "license": "MIT",
538
+ "engines": {
539
+ "node": ">= 0.6"
540
+ }
541
+ },
542
+ "node_modules/mime": {
543
+ "version": "1.6.0",
544
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
545
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
546
+ "license": "MIT",
547
+ "bin": {
548
+ "mime": "cli.js"
549
+ },
550
+ "engines": {
551
+ "node": ">=4"
552
+ }
553
+ },
554
+ "node_modules/mime-db": {
555
+ "version": "1.52.0",
556
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
557
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
558
+ "license": "MIT",
559
+ "engines": {
560
+ "node": ">= 0.6"
561
+ }
562
+ },
563
+ "node_modules/mime-types": {
564
+ "version": "2.1.35",
565
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
566
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
567
+ "license": "MIT",
568
+ "dependencies": {
569
+ "mime-db": "1.52.0"
570
+ },
571
+ "engines": {
572
+ "node": ">= 0.6"
573
+ }
574
+ },
575
+ "node_modules/minimist": {
576
+ "version": "1.2.8",
577
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
578
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
579
+ "license": "MIT",
580
+ "funding": {
581
+ "url": "https://github.com/sponsors/ljharb"
582
+ }
583
+ },
584
+ "node_modules/ms": {
585
+ "version": "2.0.0",
586
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
587
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
588
+ "license": "MIT"
589
+ },
590
+ "node_modules/negotiator": {
591
+ "version": "0.6.3",
592
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
593
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
594
+ "license": "MIT",
595
+ "engines": {
596
+ "node": ">= 0.6"
597
+ }
598
+ },
599
+ "node_modules/neo-async": {
600
+ "version": "2.6.2",
601
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
602
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
603
+ "license": "MIT"
604
+ },
605
+ "node_modules/object-inspect": {
606
+ "version": "1.13.4",
607
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
608
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
609
+ "license": "MIT",
610
+ "engines": {
611
+ "node": ">= 0.4"
612
+ },
613
+ "funding": {
614
+ "url": "https://github.com/sponsors/ljharb"
615
+ }
616
+ },
617
+ "node_modules/on-finished": {
618
+ "version": "2.4.1",
619
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
620
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
621
+ "license": "MIT",
622
+ "dependencies": {
623
+ "ee-first": "1.1.1"
624
+ },
625
+ "engines": {
626
+ "node": ">= 0.8"
627
+ }
628
+ },
629
+ "node_modules/parseurl": {
630
+ "version": "1.3.3",
631
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
632
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
633
+ "license": "MIT",
634
+ "engines": {
635
+ "node": ">= 0.8"
636
+ }
637
+ },
638
+ "node_modules/path-to-regexp": {
639
+ "version": "0.1.13",
640
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
641
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
642
+ "license": "MIT"
643
+ },
644
+ "node_modules/playwright": {
645
+ "version": "1.60.0",
646
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
647
+ "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
648
+ "license": "Apache-2.0",
649
+ "dependencies": {
650
+ "playwright-core": "1.60.0"
651
+ },
652
+ "bin": {
653
+ "playwright": "cli.js"
654
+ },
655
+ "engines": {
656
+ "node": ">=18"
657
+ },
658
+ "optionalDependencies": {
659
+ "fsevents": "2.3.2"
660
+ }
661
+ },
662
+ "node_modules/playwright-core": {
663
+ "version": "1.60.0",
664
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
665
+ "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
666
+ "license": "Apache-2.0",
667
+ "bin": {
668
+ "playwright-core": "cli.js"
669
+ },
670
+ "engines": {
671
+ "node": ">=18"
672
+ }
673
+ },
674
+ "node_modules/proxy-addr": {
675
+ "version": "2.0.7",
676
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
677
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
678
+ "license": "MIT",
679
+ "dependencies": {
680
+ "forwarded": "0.2.0",
681
+ "ipaddr.js": "1.9.1"
682
+ },
683
+ "engines": {
684
+ "node": ">= 0.10"
685
+ }
686
+ },
687
+ "node_modules/qs": {
688
+ "version": "6.15.2",
689
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
690
+ "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
691
+ "license": "BSD-3-Clause",
692
+ "dependencies": {
693
+ "side-channel": "^1.1.0"
694
+ },
695
+ "engines": {
696
+ "node": ">=0.6"
697
+ },
698
+ "funding": {
699
+ "url": "https://github.com/sponsors/ljharb"
700
+ }
701
+ },
702
+ "node_modules/range-parser": {
703
+ "version": "1.2.1",
704
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
705
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
706
+ "license": "MIT",
707
+ "engines": {
708
+ "node": ">= 0.6"
709
+ }
710
+ },
711
+ "node_modules/raw-body": {
712
+ "version": "2.5.3",
713
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
714
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
715
+ "license": "MIT",
716
+ "dependencies": {
717
+ "bytes": "~3.1.2",
718
+ "http-errors": "~2.0.1",
719
+ "iconv-lite": "~0.4.24",
720
+ "unpipe": "~1.0.0"
721
+ },
722
+ "engines": {
723
+ "node": ">= 0.8"
724
+ }
725
+ },
726
+ "node_modules/raw-body/node_modules/http-errors": {
727
+ "version": "2.0.1",
728
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
729
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
730
+ "license": "MIT",
731
+ "dependencies": {
732
+ "depd": "~2.0.0",
733
+ "inherits": "~2.0.4",
734
+ "setprototypeof": "~1.2.0",
735
+ "statuses": "~2.0.2",
736
+ "toidentifier": "~1.0.1"
737
+ },
738
+ "engines": {
739
+ "node": ">= 0.8"
740
+ },
741
+ "funding": {
742
+ "type": "opencollective",
743
+ "url": "https://opencollective.com/express"
744
+ }
745
+ },
746
+ "node_modules/raw-body/node_modules/statuses": {
747
+ "version": "2.0.2",
748
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
749
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
750
+ "license": "MIT",
751
+ "engines": {
752
+ "node": ">= 0.8"
753
+ }
754
+ },
755
+ "node_modules/safe-buffer": {
756
+ "version": "5.2.1",
757
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
758
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
759
+ "funding": [
760
+ {
761
+ "type": "github",
762
+ "url": "https://github.com/sponsors/feross"
763
+ },
764
+ {
765
+ "type": "patreon",
766
+ "url": "https://www.patreon.com/feross"
767
+ },
768
+ {
769
+ "type": "consulting",
770
+ "url": "https://feross.org/support"
771
+ }
772
+ ],
773
+ "license": "MIT"
774
+ },
775
+ "node_modules/safer-buffer": {
776
+ "version": "2.1.2",
777
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
778
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
779
+ "license": "MIT"
780
+ },
781
+ "node_modules/send": {
782
+ "version": "0.19.0",
783
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
784
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
785
+ "license": "MIT",
786
+ "dependencies": {
787
+ "debug": "2.6.9",
788
+ "depd": "2.0.0",
789
+ "destroy": "1.2.0",
790
+ "encodeurl": "~1.0.2",
791
+ "escape-html": "~1.0.3",
792
+ "etag": "~1.8.1",
793
+ "fresh": "0.5.2",
794
+ "http-errors": "2.0.0",
795
+ "mime": "1.6.0",
796
+ "ms": "2.1.3",
797
+ "on-finished": "2.4.1",
798
+ "range-parser": "~1.2.1",
799
+ "statuses": "2.0.1"
800
+ },
801
+ "engines": {
802
+ "node": ">= 0.8.0"
803
+ }
804
+ },
805
+ "node_modules/send/node_modules/encodeurl": {
806
+ "version": "1.0.2",
807
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
808
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
809
+ "license": "MIT",
810
+ "engines": {
811
+ "node": ">= 0.8"
812
+ }
813
+ },
814
+ "node_modules/send/node_modules/ms": {
815
+ "version": "2.1.3",
816
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
817
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
818
+ "license": "MIT"
819
+ },
820
+ "node_modules/serve-static": {
821
+ "version": "1.16.2",
822
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
823
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
824
+ "license": "MIT",
825
+ "dependencies": {
826
+ "encodeurl": "~2.0.0",
827
+ "escape-html": "~1.0.3",
828
+ "parseurl": "~1.3.3",
829
+ "send": "0.19.0"
830
+ },
831
+ "engines": {
832
+ "node": ">= 0.8.0"
833
+ }
834
+ },
835
+ "node_modules/setprototypeof": {
836
+ "version": "1.2.0",
837
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
838
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
839
+ "license": "ISC"
840
+ },
841
+ "node_modules/side-channel": {
842
+ "version": "1.1.0",
843
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
844
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
845
+ "license": "MIT",
846
+ "dependencies": {
847
+ "es-errors": "^1.3.0",
848
+ "object-inspect": "^1.13.3",
849
+ "side-channel-list": "^1.0.0",
850
+ "side-channel-map": "^1.0.1",
851
+ "side-channel-weakmap": "^1.0.2"
852
+ },
853
+ "engines": {
854
+ "node": ">= 0.4"
855
+ },
856
+ "funding": {
857
+ "url": "https://github.com/sponsors/ljharb"
858
+ }
859
+ },
860
+ "node_modules/side-channel-list": {
861
+ "version": "1.0.1",
862
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
863
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
864
+ "license": "MIT",
865
+ "dependencies": {
866
+ "es-errors": "^1.3.0",
867
+ "object-inspect": "^1.13.4"
868
+ },
869
+ "engines": {
870
+ "node": ">= 0.4"
871
+ },
872
+ "funding": {
873
+ "url": "https://github.com/sponsors/ljharb"
874
+ }
875
+ },
876
+ "node_modules/side-channel-map": {
877
+ "version": "1.0.1",
878
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
879
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
880
+ "license": "MIT",
881
+ "dependencies": {
882
+ "call-bound": "^1.0.2",
883
+ "es-errors": "^1.3.0",
884
+ "get-intrinsic": "^1.2.5",
885
+ "object-inspect": "^1.13.3"
886
+ },
887
+ "engines": {
888
+ "node": ">= 0.4"
889
+ },
890
+ "funding": {
891
+ "url": "https://github.com/sponsors/ljharb"
892
+ }
893
+ },
894
+ "node_modules/side-channel-weakmap": {
895
+ "version": "1.0.2",
896
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
897
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
898
+ "license": "MIT",
899
+ "dependencies": {
900
+ "call-bound": "^1.0.2",
901
+ "es-errors": "^1.3.0",
902
+ "get-intrinsic": "^1.2.5",
903
+ "object-inspect": "^1.13.3",
904
+ "side-channel-map": "^1.0.1"
905
+ },
906
+ "engines": {
907
+ "node": ">= 0.4"
908
+ },
909
+ "funding": {
910
+ "url": "https://github.com/sponsors/ljharb"
911
+ }
912
+ },
913
+ "node_modules/source-map": {
914
+ "version": "0.6.1",
915
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
916
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
917
+ "license": "BSD-3-Clause",
918
+ "engines": {
919
+ "node": ">=0.10.0"
920
+ }
921
+ },
922
+ "node_modules/statuses": {
923
+ "version": "2.0.1",
924
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
925
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
926
+ "license": "MIT",
927
+ "engines": {
928
+ "node": ">= 0.8"
929
+ }
930
+ },
931
+ "node_modules/toidentifier": {
932
+ "version": "1.0.1",
933
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
934
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
935
+ "license": "MIT",
936
+ "engines": {
937
+ "node": ">=0.6"
938
+ }
939
+ },
940
+ "node_modules/type-is": {
941
+ "version": "1.6.18",
942
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
943
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
944
+ "license": "MIT",
945
+ "dependencies": {
946
+ "media-typer": "0.3.0",
947
+ "mime-types": "~2.1.24"
948
+ },
949
+ "engines": {
950
+ "node": ">= 0.6"
951
+ }
952
+ },
953
+ "node_modules/uglify-js": {
954
+ "version": "3.19.3",
955
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
956
+ "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
957
+ "license": "BSD-2-Clause",
958
+ "optional": true,
959
+ "bin": {
960
+ "uglifyjs": "bin/uglifyjs"
961
+ },
962
+ "engines": {
963
+ "node": ">=0.8.0"
964
+ }
965
+ },
966
+ "node_modules/unpipe": {
967
+ "version": "1.0.0",
968
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
969
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
970
+ "license": "MIT",
971
+ "engines": {
972
+ "node": ">= 0.8"
973
+ }
974
+ },
975
+ "node_modules/utils-merge": {
976
+ "version": "1.0.1",
977
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
978
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
979
+ "license": "MIT",
980
+ "engines": {
981
+ "node": ">= 0.4.0"
982
+ }
983
+ },
984
+ "node_modules/vary": {
985
+ "version": "1.1.2",
986
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
987
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
988
+ "license": "MIT",
989
+ "engines": {
990
+ "node": ">= 0.8"
991
+ }
992
+ },
993
+ "node_modules/wordwrap": {
994
+ "version": "1.0.0",
995
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
996
+ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
997
+ "license": "MIT"
998
+ }
999
+ }
1000
+ }
package.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "pdf-api",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "HTML/URL to PDF generation API",
6
+ "main": "src/index.js",
7
+ "scripts": {
8
+ "start": "node src/index.js"
9
+ },
10
+ "dependencies": {
11
+ "express": "4.22.2",
12
+ "handlebars": "4.7.9",
13
+ "playwright": "1.60.0"
14
+ }
15
+ }
render.yaml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ - type: web
3
+ name: pdf-api
4
+ runtime: docker
5
+ plan: free
6
+ region: singapore
7
+ healthCheckPath: /health
8
+ envVars:
9
+ # Set this secret in the Render dashboard (kept out of the repo).
10
+ # Use the SAME value as RapidAPI's proxy secret so only RapidAPI can reach the backend.
11
+ - key: RAPIDAPI_PROXY_SECRET
12
+ sync: false
src/index.js ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const dns = require('dns').promises;
5
+ const net = require('net');
6
+ const crypto = require('crypto');
7
+ const Handlebars = require('handlebars');
8
+ const { chromium } = require('playwright');
9
+
10
+ const app = express();
11
+ app.use(express.json({ limit: '2mb' }));
12
+
13
+ const API_KEY = process.env.API_KEY;
14
+ const RAPIDAPI_PROXY_SECRET = process.env.RAPIDAPI_PROXY_SECRET;
15
+ let browser;
16
+
17
+ // Built-in templates: every .html in src/templates is registered by filename.
18
+ const TEMPLATES_DIR = path.join(__dirname, 'templates');
19
+ const TEMPLATES = {};
20
+ if (fs.existsSync(TEMPLATES_DIR)) {
21
+ for (const f of fs.readdirSync(TEMPLATES_DIR)) {
22
+ if (f.endsWith('.html')) TEMPLATES[path.basename(f, '.html')] = fs.readFileSync(path.join(TEMPLATES_DIR, f), 'utf8');
23
+ }
24
+ }
25
+
26
+ // --- Auth -------------------------------------------------------------
27
+ function safeEqual(a, b) {
28
+ if (typeof a !== 'string' || typeof b !== 'string') return false;
29
+ const ab = Buffer.from(a);
30
+ const bb = Buffer.from(b);
31
+ return ab.length === bb.length && crypto.timingSafeEqual(ab, bb);
32
+ }
33
+
34
+ function authorized(req) {
35
+ // No auth configured => allow (local/dev only).
36
+ if (!API_KEY && !RAPIDAPI_PROXY_SECRET) return true;
37
+ if (RAPIDAPI_PROXY_SECRET && safeEqual(req.get('x-rapidapi-proxy-secret'), RAPIDAPI_PROXY_SECRET)) return true;
38
+ if (API_KEY && safeEqual(req.get('x-api-key'), API_KEY)) return true;
39
+ return false;
40
+ }
41
+
42
+ // --- SSRF guard -------------------------------------------------------
43
+ function isPrivateIp(ip) {
44
+ const v4 = ip.startsWith('::ffff:') ? ip.slice(7) : ip;
45
+ if (net.isIPv4(v4)) {
46
+ const [a, b] = v4.split('.').map(Number);
47
+ return (
48
+ a === 0 || a === 10 || a === 127 ||
49
+ (a === 169 && b === 254) || // link-local + cloud metadata
50
+ (a === 172 && b >= 16 && b <= 31) ||
51
+ (a === 192 && b === 168) ||
52
+ (a === 100 && b >= 64 && b <= 127) // CGNAT
53
+ );
54
+ }
55
+ const v6 = ip.toLowerCase();
56
+ return v6 === '::1' || v6 === '::' || v6.startsWith('fe80') || v6.startsWith('fc') || v6.startsWith('fd');
57
+ }
58
+
59
+ async function hostIsPrivate(hostname) {
60
+ if (net.isIP(hostname)) return isPrivateIp(hostname);
61
+ const records = await dns.lookup(hostname, { all: true });
62
+ return records.some((r) => isPrivateIp(r.address));
63
+ }
64
+
65
+ // Validate EVERY request the page makes (initial nav, redirects, sub-resources).
66
+ // NOTE: a determined attacker could still attempt DNS rebinding between this
67
+ // check and the actual fetch; pin the resolved IP if you need more hardening.
68
+ async function installSsrfGuard(page) {
69
+ await page.route('**/*', async (route) => {
70
+ let u;
71
+ try { u = new URL(route.request().url()); } catch { return route.abort(); }
72
+ if (u.protocol === 'data:' || u.protocol === 'blob:' || u.protocol === 'about:') return route.continue();
73
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') return route.abort();
74
+ try {
75
+ return (await hostIsPrivate(u.hostname)) ? route.abort() : route.continue();
76
+ } catch {
77
+ return route.abort();
78
+ }
79
+ });
80
+ }
81
+
82
+ // --- Routes -----------------------------------------------------------
83
+ // Health check is public (no auth) so platforms can probe it.
84
+ app.get('/health', (_req, res) => res.json({ status: 'ok' }));
85
+
86
+ app.use((req, res, next) => (authorized(req) ? next() : res.status(401).json({ error: 'Unauthorized' })));
87
+
88
+ // Shared: validate input, SSRF pre-check, open page, navigate, then `produce` output.
89
+ async function render(req, res, produce) {
90
+ let { html, url, template, templateName, data } = req.body || {};
91
+ if (templateName) {
92
+ if (!TEMPLATES[templateName]) {
93
+ return res.status(400).json({ error: 'Unknown templateName. Available: ' + (Object.keys(TEMPLATES).join(', ') || 'none') });
94
+ }
95
+ template = TEMPLATES[templateName];
96
+ }
97
+ if (template) {
98
+ try {
99
+ html = Handlebars.compile(String(template))(data || {});
100
+ } catch (e) {
101
+ return res.status(400).json({ error: 'Template error: ' + e.message });
102
+ }
103
+ }
104
+ if (!html && !url) return res.status(400).json({ error: 'Provide "html", "url", "template", or "templateName".' });
105
+
106
+ // Fast pre-check for a clean error before launching a page.
107
+ if (url) {
108
+ try {
109
+ const u = new URL(url);
110
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error('Only http/https URLs are allowed.');
111
+ if (await hostIsPrivate(u.hostname)) throw new Error('Target address is not allowed.');
112
+ } catch (e) {
113
+ return res.status(400).json({ error: e.message });
114
+ }
115
+ }
116
+
117
+ const page = await browser.newPage();
118
+ try {
119
+ await installSsrfGuard(page);
120
+ await (url
121
+ ? page.goto(url, { waitUntil: 'networkidle' })
122
+ : page.setContent(html, { waitUntil: 'networkidle' }));
123
+ await produce(page);
124
+ } catch (err) {
125
+ res.status(500).json({ error: 'Render failed', detail: err.message });
126
+ } finally {
127
+ await page.close();
128
+ }
129
+ }
130
+
131
+ // POST /pdf body: { html?, url?, format?, landscape? } -> application/pdf
132
+ app.post('/pdf', (req, res) => render(req, res, async (page) => {
133
+ const { format = 'A4', landscape = false } = req.body;
134
+ const pdf = await page.pdf({ format, landscape, printBackground: true });
135
+ res.type('application/pdf').send(pdf);
136
+ }));
137
+
138
+ // POST /screenshot body: { html?, url?, fullPage?, type?, width?, height? } -> image/png|jpeg
139
+ app.post('/screenshot', (req, res) => render(req, res, async (page) => {
140
+ const { fullPage = false, type, width, height } = req.body;
141
+ if (width && height) await page.setViewportSize({ width: Number(width), height: Number(height) });
142
+ const jpeg = type === 'jpeg';
143
+ const img = await page.screenshot({ fullPage: !!fullPage, type: jpeg ? 'jpeg' : 'png' });
144
+ res.type(jpeg ? 'image/jpeg' : 'image/png').send(img);
145
+ }));
146
+
147
+ const PORT = process.env.PORT || 8080;
148
+ chromium.launch({ args: ['--no-sandbox'] }).then((b) => {
149
+ browser = b;
150
+ if (!API_KEY && !RAPIDAPI_PROXY_SECRET) {
151
+ console.warn('WARNING: no API_KEY or RAPIDAPI_PROXY_SECRET set — API is OPEN. Do not expose publicly.');
152
+ }
153
+ app.listen(PORT, () => console.log(`PDF API listening on :${PORT}`));
154
+ });
src/templates/invoice.html ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <style>
6
+ * { box-sizing: border-box; }
7
+ body { font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; color: #1f2937; margin: 0; padding: 40px; font-size: 14px; }
8
+ .header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 40px; }
9
+ .brand { font-size: 22px; font-weight: 700; color: #111827; }
10
+ .muted { color: #6b7280; }
11
+ h1 { font-size: 28px; margin: 0 0 4px; color: #111827; letter-spacing: 1px; }
12
+ .meta { text-align: right; }
13
+ .parties { margin-bottom: 32px; }
14
+ .label { text-transform: uppercase; font-size: 11px; letter-spacing: .5px; color: #9ca3af; margin-bottom: 6px; }
15
+ table { width: 100%; border-collapse: collapse; margin-bottom: 24px; }
16
+ th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .5px; color: #6b7280; border-bottom: 2px solid #e5e7eb; padding: 10px 8px; }
17
+ td { padding: 12px 8px; border-bottom: 1px solid #f3f4f6; }
18
+ .right { text-align: right; }
19
+ .totals { width: 280px; margin-left: auto; }
20
+ .totals td { border: none; padding: 6px 8px; }
21
+ .totals .grand { font-size: 18px; font-weight: 700; color: #111827; border-top: 2px solid #e5e7eb; }
22
+ .notes { margin-top: 36px; color: #6b7280; font-size: 13px; }
23
+ .footer { margin-top: 40px; text-align: center; color: #9ca3af; font-size: 12px; }
24
+ </style>
25
+ </head>
26
+ <body>
27
+ <div class="header">
28
+ <div>
29
+ <div class="brand">{{company.name}}</div>
30
+ {{#if company.address}}<div class="muted">{{company.address}}</div>{{/if}}
31
+ {{#if company.email}}<div class="muted">{{company.email}}</div>{{/if}}
32
+ </div>
33
+ <div class="meta">
34
+ <h1>INVOICE</h1>
35
+ {{#if invoice.number}}<div class="muted">#{{invoice.number}}</div>{{/if}}
36
+ {{#if invoice.date}}<div class="muted">Date: {{invoice.date}}</div>{{/if}}
37
+ {{#if invoice.due}}<div class="muted">Due: {{invoice.due}}</div>{{/if}}
38
+ </div>
39
+ </div>
40
+
41
+ <div class="parties">
42
+ <div class="label">Bill To</div>
43
+ <div><strong>{{client.name}}</strong></div>
44
+ {{#if client.address}}<div class="muted">{{client.address}}</div>{{/if}}
45
+ {{#if client.email}}<div class="muted">{{client.email}}</div>{{/if}}
46
+ </div>
47
+
48
+ <table>
49
+ <thead>
50
+ <tr>
51
+ <th>Description</th>
52
+ <th class="right">Qty</th>
53
+ <th class="right">Price</th>
54
+ <th class="right">Amount</th>
55
+ </tr>
56
+ </thead>
57
+ <tbody>
58
+ {{#each items}}
59
+ <tr>
60
+ <td>{{this.description}}</td>
61
+ <td class="right">{{this.quantity}}</td>
62
+ <td class="right">{{this.price}}</td>
63
+ <td class="right">{{this.amount}}</td>
64
+ </tr>
65
+ {{/each}}
66
+ </tbody>
67
+ </table>
68
+
69
+ <table class="totals">
70
+ {{#if subtotal}}<tr><td>Subtotal</td><td class="right">{{subtotal}}</td></tr>{{/if}}
71
+ {{#if tax}}<tr><td>Tax</td><td class="right">{{tax}}</td></tr>{{/if}}
72
+ <tr><td class="grand">Total</td><td class="right grand">{{total}}</td></tr>
73
+ </table>
74
+
75
+ {{#if notes}}<div class="notes">{{notes}}</div>{{/if}}
76
+ {{#if company.name}}<div class="footer">Thank you for your business — {{company.name}}</div>{{/if}}
77
+ </body>
78
+ </html>