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
34 changes: 30 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,30 @@ TLS_PORT=8443

# Optional app-managed supplemental CA bundle for outbound HTTPS.
# OUTBOUND_CA_BUNDLE_FILE=/absolute/path/to/outbound-extra-ca.pem
#
# Node's own extra-CA variable is honoured as a fallback for the same purpose.
# NODE_EXTRA_CA_CERTS=/absolute/path/to/outbound-extra-ca.pem

# Hosts outbound fetches may reach WITHOUT the private-network SSRF check.
# Comma-separated host[:port]; matched exactly and case-normalized against the
# URL host (with port) or hostname (without). No wildcards, no suffix matching:
# example.com does NOT match api.example.com.
#
# WARNING: a listed host short-circuits BOTH assertPublicAddress() and DNS
# resolution, so an entry resolving to a loopback/link-local/private address
# WILL be fetched. Intended for a local fixture or test-harness origin (this is
# what `npm run postman:full` uses); do not use it to force through a public
# host the policy rejected. See DEVELOPMENT.md → Outbound host allowlist.
# OUTBOUND_ALLOWED_HOSTS=127.0.0.1:19090

# Optional rate limiting and logging.
# RATE_LIMIT_WINDOW_MS=900000
# RATE_LIMIT_MAX=100
#
# Status/health routes use their own limiter so probes do not share the main
# API request budget.
# STATUS_RATE_LIMIT_WINDOW_MS=60000
# STATUS_RATE_LIMIT_MAX=60
# LOG_LEVEL=debug

# Optional API access control. When set, scraper and description endpoints
Expand Down Expand Up @@ -118,7 +138,13 @@ TLS_PORT=8443
# TOGETHER_MAX_TOKENS=160
# TOGETHER_PROMPT=Write concise alt text for this image.

# Development TLS defaults already point to the checked-in localhost certs.
# Uncomment only when overriding them.
# TLS_KEY=../../certs/localhost-key.pem
# TLS_CERT=../../certs/localhost.pem
# TLS is enabled by default.
# In development the app uses certs/localhost*.pem when present (they are NOT
# checked in — certs/ is gitignored); otherwise it generates a temporary
# self-signed localhost certificate in-process.
# Set TLS_ENABLED=false only behind a trusted TLS-terminating platform edge.
# TLS_ENABLED=true
#
# Prefer absolute paths; relative paths resolve against src/infrastructure/.
# TLS_KEY=/absolute/path/to/localhost-key.pem
# TLS_CERT=/absolute/path/to/localhost.pem
21 changes: 10 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,28 +50,27 @@ jobs:

node scripts/ci/classify-changed-paths.js --input changed-files.txt

# Runs unconditionally, unlike the other gates' docs-only skips. docs:validate
# now asserts across the repository, not just within the changed files: it
# holds every env var read by config/*.js to being documented. A change that
# adds one without touching any Markdown is exactly the case worth catching,
# and a docs_changed condition would skip precisely that change.
docs:
name: docs
needs:
- changes
runs-on: ubuntu-latest
timeout-minutes: 5
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: .nvmrc
- name: Setup project
uses: ./.github/actions/setup-node-project

- name: Validate docs
if: needs.changes.outputs.docs_changed == 'true'
run: npm run docs:validate

- name: Skip docs validation when no docs changed
if: needs.changes.outputs.docs_changed != 'true'
run: echo "No docs-only validation needed for this change."
- name: Lint Markdown
run: npm run docs:lint

actionlint:
name: actionlint
Expand Down
29 changes: 29 additions & 0 deletions .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
// Structure and heading hierarchy are what this gate is for. Three default
// rules are off because they contradict deliberate choices in these docs
// rather than catching mistakes in them:
//
// MD013 line-length prose, tables, and command lines are intentionally
// long; enforcing 80 columns would rewrap every file
// and change nothing about correctness.
// MD033 no-inline-html the README banner is a <div>/<img> block, and the
// img is exactly where the alt attribute has to live.
// MD041 first-line-h1 the README opens with that banner, not a heading.
// MD029 ol-prefix the runbook sequences interleave unindented code
// fences, which ends each list and starts the next at
// its own number. That renders correctly (<ol start=N>)
// and re-indenting eight fences to satisfy the rule
// would churn the document without fixing anything.
//
// Everything else stays on, including the heading-hierarchy rules this gate
// is for: MD001 increment, MD024 duplicates, MD025 single H1, MD022 spacing.
"config": {
"default": true,
"MD013": false,
"MD033": false,
"MD041": false,
"MD029": false
},
"globs": ["**/*.md"],
"gitignore": true
}
30 changes: 30 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -463,8 +463,38 @@ Public traffic is still end-to-end HTTPS to the client; only the edge→app hop
| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `OUTBOUND_CA_BUNDLE_FILE` | No | unset | App-managed supplemental PEM bundle used to extend Node trust for outbound HTTPS. |
| `OUTBOUND_ALLOWED_HOSTS` | No | unset | Comma-separated `host[:port]` allowlist for outbound fetches. **Each entry bypasses the private-network SSRF guard** — see [Outbound host allowlist](#outbound-host-allowlist) before setting it. |
| `NODE_EXTRA_CA_CERTS` | No | unset | Node-supported extra CA file. This app also accepts it as a fallback. Not validated by Joi. |

#### Outbound host allowlist

`OUTBOUND_ALLOWED_HOSTS` names hosts that outbound fetches may reach **without
the private-network check**. Read this before setting it: it is the one setting
here that can turn the scraper into an SSRF primitive.

**Syntax.** A comma-separated list of `host[:port]` entries, validated at boot:

```dotenv
OUTBOUND_ALLOWED_HOSTS=127.0.0.1:19090,fixtures.internal:8080
```

**Semantics.** Matching is exact and case-normalized against the URL's `host`
(with port) or `hostname` (without). There are **no wildcards and no suffix
matching** — `example.com` does not match `api.example.com`, and
`example.com:8443` does not match `example.com`. Entries are compared verbatim
(`src/infrastructure/outboundUrlPolicy.js`).

**What it bypasses.** A matching host returns *before* `assertPublicAddress()`
and before DNS resolution, which are the guards that stop the app from being
pointed at loopback, link-local, or private-range addresses. An allowlisted host
that resolves to `169.254.169.254` will be fetched. That is the entire point of
the setting, and the entire risk of it.

**Use it for** a local fixture or test-harness origin — which is what
`npm run postman:full` does. **Do not use it** to work around a legitimate
rejection of a public host; nothing about the allowlist is scoped to a path,
method, or protocol.

### Worker, Proxy, and Scraper Runtime Controls

| Variable | Required | Default | Description |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div align="center">
<img src="https://raw.githubusercontent.com/jsugg/alt-text-generator/main/.github/assets/alt-text-generator.png" width="1000">
<img src="https://raw.githubusercontent.com/jsugg/alt-text-generator/main/.github/assets/alt-text-generator.png" width="1000" alt="">
</div>

# Alt-Text 4 All
Expand Down
Loading
Loading