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: 43 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Bug report
description: Something in sitepass misbehaves
body:
- type: dropdown
id: adapter
attributes:
label: Adapter
description: Which integration are you using?
options:
- cloudflare
- netlify
- next
- astro
- sveltekit
- express
- hono
- bun
- reverse proxy (sitepass proxy)
- CLI (sitepass init)
- core (createGate)
validations:
required: true
- type: input
id: versions
attributes:
label: Versions
description: sitepass version, Node/Bun version, and the framework version if relevant
placeholder: sitepass 0.1.1, Node 22.11, Next 16.2
validations:
required: true
- type: textarea
id: repro
attributes:
label: What happened, and how to reproduce it
description: Config (redact your password/secret!), the request you made, what you expected, what you got.
validations:
required: true
- type: markdown
attributes:
value: >
**Security issues:** please do not file them here — use the private
reporting channel described in
[SECURITY.md](https://github.com/PeterM45/sitepass/blob/main/SECURITY.md).
16 changes: 16 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!-- Thanks! Two notes before you open this:

1. Read CONTRIBUTING.md — especially the guardrails (no client-side gates,
Web Crypto only in core, zero runtime dependencies).
2. Security fixes should go through private reporting (SECURITY.md), not a PR.
-->

## What

<!-- One or two sentences: what changes and why. -->

## Checklist

- [ ] `bun run typecheck && bun run check && bun run test` pass
- [ ] New behavior is covered by a test
- [ ] No new runtime dependencies
18 changes: 18 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Keeps the SHA-pinned actions fresh (Dependabot updates the pin and the
# version comment together) and surfaces dev-dependency updates. The published
# package has zero runtime dependencies, so npm updates only touch the
# toolchain.
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
- package-ecosystem: bun
directory: /
schedule:
interval: weekly
groups:
dev-dependencies:
patterns:
- '*'
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,48 @@
name: CI

# Run PR branches once (via pull_request), not twice; pushes to main and tags
# still get their own run.
on:
push:
branches: [main]
tags: ['v*']
pull_request:

# Least privilege: the workflow only reads the repo. Raise per-job if a step ever
# needs more (e.g. a publish job would add id-token: write).
permissions:
contents: read

# A superseded push cancels the still-running CI for the same ref.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
quality:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
# Actions are pinned to commit SHAs (not mutable tags) so a retagged or
# compromised release can't silently change what runs in CI.
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false # don't leave repo creds on the runner
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
Comment on lines 28 to 31

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Harden CI: disable persisted checkout credentials and Bun executable cache

  • Lines 28 & 60 (actions/checkout): add with: persist-credentials: false to avoid leaving auth/SSH credentials on the runner.
  • Lines 29 & 61 (oven-sh/setup-bun): add with: no-cache: true to disable Bun executable caching.
Suggested patch
-      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
-      - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
+      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+        with:
+          persist-credentials: false
+      - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
+        with:
+          no-cache: true
...
-      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
-      - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
+      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+        with:
+          persist-credentials: false
+      - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
+        with:
+          no-cache: true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
no-cache: true
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 28-28: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 29-29: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): enables caching by default

(cache-poisoning)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 28 - 29, Update the GitHub Actions
steps that use actions/checkout and oven-sh/setup-bun: for the actions/checkout
steps (identified by uses: actions/checkout@...), add a with block setting
persist-credentials: false to avoid leaving auth/SSH credentials on the runner;
for the oven-sh/setup-bun steps (identified by uses: oven-sh/setup-bun@...), add
a with block setting no-cache: true to disable Bun executable caching. Ensure
you apply these changes to both occurrences of each action in the workflow so
the runner and cache hardening are consistent.

# Pin Node for the coverage gate too: v8 coverage output shifts between
# Node majors, and the runner's system Node changes when images rotate.
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- run: bun install --frozen-lockfile
- run: bun run typecheck
- run: bun run check
- run: bun run build
# Execute the built artifact: import every dist entry in both formats and
# run the CLI, so a tsup/exports-map regression can't ship green. publint
# validates the publish metadata against the same build.
- run: node scripts/smoke-dist.mjs
- run: bunx publint
# Coverage gate on the library code (excludes the CLI entrypoint). Run on
# the runner's Node so the v8 provider collects coverage correctly.
- run: node ./node_modules/.bin/vitest run --coverage
Expand All @@ -31,12 +53,15 @@ jobs:

test:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
node-version: [20, 22, 24]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false # don't leave repo creds on the runner
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
Expand Down
18 changes: 17 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,44 @@ name: Publish
on:
push:
tags:
- 'v*'
- 'v[0-9]*'

permissions:
contents: read

jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
id-token: write # required for npm provenance (OIDC)
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false # don't leave repo creds on the runner
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- run: bun install --frozen-lockfile
# Refuse a tag that doesn't match package.json, so a mistagged push can't
# publish a mismatched version (or provenance pointing at the wrong ref).
- name: Verify tag matches package.json version
run: |
version="$(node -p "require('./package.json').version")"
if [ "${GITHUB_REF_NAME#v}" != "$version" ]; then
echo "Tag $GITHUB_REF_NAME does not match package.json version $version" >&2
exit 1
fi
- run: bun run typecheck
- run: bun run check
- run: node ./node_modules/.bin/vitest run
# Build and execute the exact artifact that ships before publishing it.
- run: bun run build
- run: node scripts/smoke-dist.mjs
- run: bunx publint
# npm (not bun) publishes with provenance; prepack runs tsup to build dist.
- run: npm publish --provenance --access public
env:
Expand Down
111 changes: 110 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,114 @@ All notable changes to this project are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

Because of the two behavior changes below, this should ship as **0.2.0**, not a
0.1.x patch — `^0.1` consumers should not pick it up automatically.

Heads-up for upgraders, two deliberate behavior changes:

- **Existing sessions are invalidated once on upgrade.** Tokens now bind a
digest of the password into the signed message, so visitors log in once more
with the unchanged password — and from then on, rotating the password
actually revokes outstanding sessions.
- **Secrets shorter than 16 characters now fail closed** (503 with the
not-configured page) instead of silently signing tokens with a
brute-forceable key. Anything written by `sitepass init` is unaffected.

### Security

- **Core:** `publicPaths` matching now rejects literal dot-segments, backslash,
and path-parameter (`;`) segments, plus their encoded forms (`%2e`, `%2f`,
`%5c`, `%3b`, and a stray `%25` that a double-decoding origin could unwrap),
closing the remaining traversal route: the reverse proxy and Express adapters
pass the raw request target into the gate, so `/assets/../secret` matched an
`/assets` prefix verbatim while resolving elsewhere at the origin. (The basic
encoded form was fixed in 0.1.1; the edge adapters were never affected because
`URL` normalizes the pathname.)
- **Core:** a `publicPaths` entry of `/` is now an exact match on the root
path. Previously it un-gated the entire site, because trailing-slash
normalization reduced it to a prefix that matched every path. Empty entries
are ignored.
- **Core:** rotating `SITEPASS_PASSWORD` now invalidates all outstanding
sessions (previously only rotating the secret did, and nothing documented
that).
- **All adapters:** the login `POST` body is read with a 64 KiB cap and fails
closed with `413` on every adapter (the reverse proxy caps its login body at
64 KiB too, separately from its larger forward-body limit). Previously only
Express was capped; Bun and Hono — self-hosted runtimes with no platform
limit — buffered without bound.
- **Reverse proxy:** the gate's own session cookie and the `x-sitepass-bypass`
credential are stripped before forwarding, so origin-side logs can no longer
capture a replayable credential. Other cookies forward unchanged. The
`X-Forwarded-For`/`-Proto`/`-Host` headers are set authoritatively from the
proxy-observed connection, so a client cannot spoof them to the origin.

### Fixed

- **Core:** the login `POST` is handled before `publicPaths` matching, so an
entry covering `loginPath` can no longer make logging in impossible.
- **Cloudflare:** `export const onRequest: PagesFunction<Env> = gate()` now
typechecks (the env slice no longer demands an index signature), and
non-string bindings count as unset instead of leaking into the gate.
- **Bun:** the wrapper is generic over the handler's rest arguments, so the
`(req, server) => server.upgrade(req)` websocket pattern compiles and
`server` is actually forwarded.
- **Netlify:** importing the adapter outside the Edge runtime fails closed
(503) instead of throwing `ReferenceError: Netlify is not defined`.
- **CLI:** `--help`/`-h` print usage and exit 0 (previously "Unknown command"
and exit 1); `sitepass init --help` prints usage instead of starting an
interactive init; `.env` loading no longer silently does nothing on Node
20.0–20.11; unknown flags are an error instead of being ignored; a missing
`SITEPASS_PASSWORD` warns at proxy startup just like a missing secret.
- **Docs:** the README no longer claims the SvelteKit adapter gates static
output (prerendered pages and `/_app` client assets bypass server hooks),
documents the Bun adapter's `gate(handler, options)` signature, and corrects
the localhost/`Secure`-cookie guidance (Chrome and Firefox allow it over
plain-HTTP localhost; Safari does not).

### Added

- **Bypass token for CI, E2E, and uptime monitors:** set
`SITEPASS_BYPASS_TOKEN` (or the `bypassToken` option) and send the
`x-sitepass-bypass` header to pass the gate without a session. Constant-time
comparison, same as the password check.
- **Logout:** `GET <loginPath>/logout` clears the session cookie and redirects
to `/`.
- **`renderLoginPage` option** to fully replace the built-in login page
(localization, logos), plus an exported `escapeHtml` helper for safe
interpolation.
- **`onAuthFailure` option:** an observer called on every failed login
attempt, for fail2ban-style logging on platforms without access logs. It
receives only a redacted `{ method, path }` view — never the submitted
password or session cookie — so wiring it to logs can't persist credentials.
- **`cookieSecure: false` option** (and the proxy's `--insecure-cookie` flag)
for plain-HTTP LAN deployments, which previously failed as a silent login
loop.
- **`maxBodyBytes` option on every adapter** (login body cap; documented now —
previously Express-only and undocumented).
- **`sitepass/proxy` export:** `startProxy(options)` is now importable for
programmatic use; it accepts every gate option. (The files already shipped
in the tarball but were unreachable.)
- **Reverse proxy:** sends authoritative `X-Forwarded-For`/`-Proto`/`-Host` to
the origin; the CLI exposes gate options as flags (`--public-paths`,
`--login-path`, `--cookie-name`, `--session-seconds`, `--bypass-token`,
`--env-file`), with `--session-seconds` validated like `--port`.
- **CLI:** `--version`/`-v`, and `--env-file` for monorepos.
- `"sideEffects": false` for better tree-shaking, and `CHANGELOG.md` now ships
in the npm tarball.
- **Types:** every adapter exports its options type (`CloudflareGateOptions`,
…) and context interface; `publicPaths` accepts `readonly string[]`; the
declarations are precise under `exactOptionalPropertyTypes`;
`@types/express` is declared as an optional peer dependency; a
`typesVersions` map resolves subpath types under legacy node10 module
resolution (TypeScript consumers on `moduleResolution: node` previously got
no types for `sitepass/cloudflare` et al).
- **CI:** every built `dist` entry is now imported in both formats (plus a CLI
run and `publint`) before merge and before publish; the publish workflow
refuses a tag that does not match `package.json`'s version; Dependabot keeps
the SHA-pinned actions fresh; issue and PR templates.

## [0.1.1] - 2026-06-05

Security and hardening release. **Upgrading is recommended for all users**, in
Expand Down Expand Up @@ -36,8 +144,9 @@ particular anyone using the Express adapter or the `sitepass init` CLI.
patched releases. These are build-time only — the published package has no
runtime dependencies, so consumers were never exposed.

## [0.1.0]
## [0.1.0] - 2026-06-04

Initial release.

[0.1.1]: https://github.com/PeterM45/sitepass/releases/tag/v0.1.1
[0.1.0]: https://www.npmjs.com/package/sitepass/v/0.1.0
Comment on lines 151 to +152

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Inconsistent version link targets.

The 0.1.1 link points to a GitHub release tag, while 0.1.0 points to the npm package page. For consistency and to follow common changelog conventions, both should point to GitHub release tags (e.g., https://github.com/PeterM45/sitepass/releases/tag/v0.1.0) or to GitHub compare URLs showing the diff.

📝 Suggested fix for consistency
-[0.1.0]: https://www.npmjs.com/package/sitepass/v/0.1.0
+[0.1.0]: https://github.com/PeterM45/sitepass/releases/tag/v0.1.0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[0.1.1]: https://github.com/PeterM45/sitepass/releases/tag/v0.1.1
[0.1.0]: https://www.npmjs.com/package/sitepass/v/0.1.0
[0.1.1]: https://github.com/PeterM45/sitepass/releases/tag/v0.1.1
[0.1.0]: https://github.com/PeterM45/sitepass/releases/tag/v0.1.0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 147 - 148, Update the inconsistent link target for
the 0.1.0 entry so it matches the 0.1.1 pattern: replace the npm URL used in the
[0.1.0] reference with the GitHub release tag URL (e.g., change
"https://www.npmjs.com/package/sitepass/v/0.1.0" to
"https://github.com/PeterM45/sitepass/releases/tag/v0.1.0") so both [0.1.1] and
[0.1.0] point to GitHub release tags.

10 changes: 6 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Thanks for helping out. This is a small, deliberately simple package, and the go

```sh
bun install
bun run build # tsup: ESM + CJS + .d.ts for every entry
bun run build # tsup: ESM + CJS + .d.ts for every package entry (the CLI bin is ESM-only, no declarations)
bun run test # vitest
bun run typecheck # tsc --noEmit
bun run check # biome (lint + format check)
Expand Down Expand Up @@ -40,10 +40,12 @@ If a file feels over-built, it is. Delete until it is obvious.

## The adapter contract

Every adapter does the same three steps and should be short enough to read at a glance:
Every adapter does the same three steps:

1. **Normalize.** Build a `GateRequest` from the host's request: method, pathname, search string, the gate cookie value, and the raw body only when it is a POST to the login path.
1. **Normalize.** Build a `GateRequest` from the host's request: method, pathname, search string, the gate cookie value, the `x-sitepass-bypass` header, and the raw body only when it is a POST to the login path.
2. **Handle.** `await gate.handle(request)`.
3. **Translate.** Map the `GateResult`: `pass` continues to the app (`next()` or the wrapped handler), `redirect` becomes a 302 with `Location` and `Set-Cookie`, `html` becomes a response with the given status, body, and headers.

Each adapter reads `SITEPASS_PASSWORD` and `SITEPASS_SECRET` from its own environment and passes them into `createGate`. The core never reads environment variables. When adding an adapter, pull the framework's current middleware docs first and match how that framework writes middleware.
For hosts that speak web `Request`/`Response` this is already written: `src/web.ts` (internal, not a package export) does normalize + translate with a capped login-body read, so those adapters reduce to reading their environment and calling `(await gateWebRequest(g, request, maxBodyBytes)) ?? next()` (the `await` matters — a pending Promise is never nullish). The Node-side consumers (Express, the proxy) share the capped body readers in `src/node-body.ts`. New adapters should reuse these instead of re-implementing the plumbing — and add a `describeAdapterConformance` driver in `test/adapters.test.ts`, which buys the full conformance suite (login, cookie pass, body cap, logout, bypass) for ~10 lines.

Each adapter reads `SITEPASS_PASSWORD`, `SITEPASS_SECRET`, and `SITEPASS_BYPASS_TOKEN` from its own environment and passes them into `createGate`. The core never reads environment variables. When adding an adapter, pull the framework's current middleware docs first and match how that framework writes middleware.
Loading
Loading