A CORS-enabled proxy service for the Pinboard API with enhanced security features.
- CORS Support: Configurable cross-origin resource sharing with JSON error responses
- Security Headers: Helmet.js integration for secure HTTP headers
- Rate Limiting: 100 requests per 15 minutes per IP address
- Header-based Authentication: Keeps Pinboard credentials out of URLs
- XML to JSON Conversion: Automatic XML response conversion using fast-xml-parser
- Health Check Endpoint: Monitor service status at
/health - Graceful Shutdown: Proper handling of SIGTERM/SIGINT signals
- Error Handling: Comprehensive error handling with consistent JSON responses
- Preview API: Server-side endpoint that scrapes Twitter/Open Graph metadata alongside Pinboard suggestions
- Preview API: Server-side endpoint that scrapes Twitter/Open Graph metadata alongside Pinboard suggestions
- Node.js 18+ (matches the Heroku-24 build image)
- npm 8+
- Your Pinboard API token (
username:XXXXXXas shown in Pinboard settings). The bridge does not accept raw account passwords.
git clone git@github.com:rossshannon/pinboard-bridge.git
cd pinboard-bridge
npm install
cp .env.example .env| Variable | Description | Default | Required |
|---|---|---|---|
PORT |
Server port | 1337 | No |
NODE_ENV |
Environment mode | development | No |
ALLOWED_ORIGINS |
Comma-separated list of allowed CORS origins | (all origins) | No |
CORS strategy: The bridge still allows every origin if ALLOWED_ORIGINS is unset (for backwards compatibility with self-hosted setups). For any public deployment you should list the exact origins that are allowed to call the proxy. The sample .env.example defaults to the Pincushion frontend at https://rossshannon.github.com:
ALLOWED_ORIGINS=https://rossshannon.github.com- Browser/extension clients must send credentials through the
Authorizationheader. - Incoming
auth_tokenquery parameters are stripped before forwarding to Pinboard, preventing accidental leaks through logs, history, or shared URLs. Authorization: Basic <base64(username:token)>→ proxied upstream using HTTP Basic. (Base64 encode the literalusername:tokenstring provided by Pinboard.)Authorization: Bearer username:token→ rewritten as the upstreamauth_tokenquery parameter and kept out of browser-visible URLs.
- 100 requests per 15 minutes are allowed per source IP. Heroku users should ensure
app.set('trust proxy', 1)remains enabled so the limiter sees client IPs. - When throttled, responses include standard
RateLimit-*headers so the UI can surface “retry-after” information. /posts/suggest-with-previewadds a per-token limiter (30 requests/minute) to prevent abusive preview scraping.
npm run dev
# or specify env vars
PORT=1337 ALLOWED_ORIGINS=https://rossshannon.github.com npm startHealth check:
curl http://localhost:1337/healthProxy usage example (Pinboard “all posts” endpoint using an auth token exposed through Bearer):
curl \
-H "Origin: https://rossshannon.github.com" \
-H "Authorization: Bearer username:YOURTOKEN" \
"http://localhost:1337/v1/posts/all?format=json"| Method | Path | Description |
|---|---|---|
GET |
/health |
Returns service status, uptime, and timestamp. Useful for uptime monitors. |
GET |
/v1/* |
Forwards requests to https://api.pinboard.in with the same path/query, after injecting auth info and normalizing responses. |
GET |
/posts/suggest-with-preview |
Calls Pinboard’s posts/suggest and, in parallel, fetches preview metadata (Twitter Card/Open Graph) for the supplied URL. |
Responses default to JSON. If Pinboard returns XML (the default), the bridge converts it to JSON via fast-xml-parser before sending the response back to the browser.
This endpoint keeps preview scraping on the server (no extra browser permissions) while returning the same suggestions data as Pinboard’s posts/suggest.
Query parameters
url(required): The bookmark target. Must behttp://orhttps://and cannot point to localhost/loopback/RFC1918 hosts.
Authentication
- Same
Authorizationheader scheme as the other routes (Basic or Bearer, both usingusername:token). Incomingauth_tokenquery params are ignored.
Response structure
{
"suggestions": { "popular": [], "recommended": [] },
"preview": {
"url": "https://example.com/article",
"title": "Amazing Gadget",
"description": "All the reasons it is amazing.",
"imageUrl": "https://example.com/images/gadget.png",
"siteName": "Example News",
"siteHandle": "@example",
"siteHandleUrl": "https://twitter.com/example",
"siteDomain": "example.com",
"cardType": "summary_large_image",
"themeColor": "#0a84ff",
"faviconUrl": "https://example.com/favicon.ico",
"fetchedAt": "2025-11-15T20:05:00.000Z"
},
"previewStatus": "fresh",
"previewError": "optional explanation when preview scraping fails"
}preview is omitted (or null) when no metadata is found. If the metadata fetch fails entirely (timeout, unsupported MIME type, invalid URL, etc.), the endpoint still returns the Pinboard suggestions but adds a previewError string and sets previewStatus to error. When data is returned straight from a fresh fetch, previewStatus is fresh (future updates may re-use this flag to signal cached responses). Whenever the site exposes a Twitter creator/site tag, the payload also includes siteHandleUrl so UIs can link to the social profile directly.
Limits & logging
- Preview fetches are limited to 30 requests per minute per Authorization token.
- Each request logs
{user, targetHost, outcome}(preview_generated,preview_not_found, orpreview_failed) to help spot misuse without storing the raw token.
-
Provision an app on the
heroku-24stack. -
Set config vars (at minimum
NODE_ENV=productionand your chosenALLOWED_ORIGINS). -
Push the main branch:
git push heroku main
-
Tail logs with
heroku logs --tailif you need to debug startup issues. (This service intentionally avoids application-level request logging to keep credentials out of Heroku Logplex.)
Any Node.js host that exposes port 1337 (or a configured alternative) works. Remember to configure reverse proxies (NGINX, Cloudflare, etc.) to forward Authorization headers untouched and to respect the rate-limiting proxy settings.
- Remove
auth_tokenquery params from all clients; instead send headers as described above. - Because request logging was removed, make sure your external monitoring still captures health and latency (e.g., via uptime checks or reverse-proxy metrics).
- Update any infrastructure-as-code scripts to include the new default
ALLOWED_ORIGINSor your production hostname.
| Symptom | Likely Cause | Fix |
|---|---|---|
401 Authorization header required |
Missing/typoed Authorization header |
Ensure the client always sends either Basic or Bearer credentials. |
401 Invalid Bearer authorization header |
Bearer token missing username: prefix |
Format should be username:token, exactly as Pinboard displays under “API token”. |
403 Origin not allowed by CORS policy |
The requesting site is absent from ALLOWED_ORIGINS |
Add the origin (scheme + host, optional port) to the env var or leave it blank for development. |
504 Gateway timeout |
Pinboard took longer than 30 seconds to respond | Retry later; Pinboard occasionally rate limits. Consider adding caching upstream. |
Too many requests |
Rate limiter tripped | Reduce polling frequency or implement client-side caching/backoff. |
- Helmet.js security headers
- CORS origin validation with explicit 403 JSON responses for disallowed origins
- Rate limiting per IP
- Per-token preview throttling and structured preview logging
- 30-second request timeout
- Sanitized error messages without leaking upstream responses
- Authorization headers only (tokens never logged in URLs)
- Require Authorization headers and ignore inbound
auth_tokenquery parameters - Support
Authorization: Bearer username:tokenso clients can keep tokens out of URLs while Pinboard still receivesauth_token - Remove request logging to prevent credential exposure in logs
- Return JSON 403 responses for blocked origins
- Document recommended
ALLOWED_ORIGINSvalue (https://rossshannon.github.com) and update.env.example - Bump dependencies to drop unused logging package
- Add
/posts/suggest-with-previewendpoint with URL validation, metadata parsing, and per-token throttling - Include
themeColor,faviconUrl, andpreviewStatusso the frontend can style cards and detect stale previews
- Updated dependencies (axios, express)
- Replaced sax2json with fast-xml-parser
- Added helmet for security headers
- Added express-rate-limit for rate limiting
- Added morgan for request logging
- Added configurable CORS with origin whitelist
- Added health check endpoint
- Added graceful shutdown handling
- Added request timeout (30s)
- Improved error handling and consistency
- Upgraded to Heroku stack heroku-24
- Previous stable release