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
12 changes: 9 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
# Copy to .env for optional local OAuth. Blank credentials keep providers disabled.
# Register the exact callbacks documented in OAUTH.md. Never commit real secrets.
PORT=3000
PUBLIC_ORIGIN=http://127.0.0.1:3000
# Register the exact callbacks listed in examples/better-auth/README.md, on the
# origin `pnpm dev` prints for this PORT. Never commit real secrets.
PORT=8082
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
APPLE_CLIENT_ID=
APPLE_CLIENT_SECRET=
FACEBOOK_CLIENT_ID=
FACEBOOK_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
MICROSOFT_CLIENT_ID=
MICROSOFT_CLIENT_SECRET=
59 changes: 51 additions & 8 deletions .github/audit/security.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
# Domain: security

**Scope — this file, and no other:**
**Scope — these, and no others:**

- `SECURITY.md`
- `SECURITY.md`: every `FAIL IF`, and the code and tests each bullet names
- `packages/stripe/src/`: the `Billing` class in `index.ts` and `db.ts`, and
the StripeDev simulator in `testing.ts` that the billing suites run against
- `packages/stripe/migrations/`: the `pgstencil_billing` schema and its
`owner_access` view
- the billing suites: `tests/integration/billing.test.ts`,
`tests/integration/billing-security.test.ts`,
`tests/integration/billing-workers.test.ts` and
`tests/integration/billing-http.test.ts`

**Output file:** `audit-report.md`

This is a code-and-policy audit of what the packed packages guarantee to a
consuming application. Everything `SECURITY.md` assigns to the application —
TLS, the origin gate, page CSP, secret storage, database provisioning, provider
registration — is out of scope; so are `@pgstencil/stripe` and the original
code/link `Auth` exports, which carry no rules yet.
registration, and for billing the owner's authentication, CSRF on its billing
routes, raw-body webhook delivery, Stripe key storage and the Stripe account's
configuration — is out of scope; so is the rest of `@pgstencil/stripe` that
`SECURITY.md` lists under "What is not defended".

## Mechanical pass

Expand All @@ -21,12 +31,18 @@ each bullet names.
```sh
pnpm db:verify # migrations apply and match the committed schema
pnpm test:unit # diagnostics allowlist and the other unit rules
pnpm test:better-auth # the session, CSRF, email-code, OAuth and linking suites
pnpm exec vitest run tests/integration/better-auth.test.ts \
tests/integration/better-auth-workers.test.ts \
tests/integration/better-auth-oauth.test.ts # the session, CSRF, email-code, OAuth and linking suites
pnpm exec vitest run tests/integration/billing.test.ts \
tests/integration/billing-security.test.ts \
tests/integration/billing-workers.test.ts \
tests/integration/billing-http.test.ts # the webhook, checkout and entitlement suites
pnpm packages:verify # packs, installs into a clean project, checks provenance
```

`test:better-auth` and `packages:verify` need a working Docker daemon (see
`compose.yaml`). If the daemon is unreachable, every check those suites pin is
The auth suites, the billing suites and `packages:verify` need a working
Docker daemon (see `compose.yaml`). If the daemon is unreachable, every check those suites pin is
`UNVERIFIABLE` — say so and say why. **Never record a check as PASS on the
strength of a test you did not run**, and never quietly skip one.

Expand All @@ -51,11 +67,19 @@ The remaining bullets are read rather than run:
- The `Packed provenance` bullets are read over `scripts/build-packages.ts`,
`scripts/pack-packages.ts` and `scripts/verify-packages.ts`, with
`pnpm packages:verify` as the evidence.
- The `Stripe webhooks`, `Checkout` and `Entitlement` bullets are pinned by the
billing suites, and each also names the `Billing` members to read in
`packages/stripe/src/index.ts`. Read those members in full: several clauses
(the tolerance, the injected clock, the order of the ledger, resync and
`apply`, what `synchronize` checks) are statements about the code that a
passing test only samples. Compare `SUBSCRIBED` with the status list in
`007_billing_owner_access.sql` character for character.

## Qualitative pass

Be adversarial, and go past the `FAIL IF` list. Read
`packages/auth/src/better-auth*.ts`, `packages/pgstencil/src/*.ts`, and the
`packages/auth/src/better-auth*.ts`, `packages/pgstencil/src/*.ts`,
`packages/stripe/src/index.ts`, `packages/stripe/migrations/*.sql`, and the
suites under `tests/` — including what they do _not_ cover. Ask at least:

- **Can a caller influence where an OAuth callback lands, or reuse state across
Expand Down Expand Up @@ -93,6 +117,25 @@ suites under `tests/` — including what they do _not_ cover. Ask at least:
`pgstencil.json` migrations path, and the bundled `compose.yaml` fallback all
cross into a child process. Look for string-built commands, unquoted
interpolation, and a path that escapes the project root.
- **Can anything from a webhook body reach a billing table, or grant access?**
Follow every read of `event` in `webhook` and `completedCheckout` in
`packages/stripe/src/index.ts`. The body may choose whose state to resync and
which row `context.subscription` reads back; anything more — a status, a
period end, a Price, an owner taken from metadata — is the finding.
- **Can one owner end up with two chargeable subscriptions, a second trial, or
another owner's Checkout?** Trace `checkout` through `account`, `customer`,
`reconcile`, the locked transaction and the Stripe call made after it
commits, and `cancelCheckout` and `replace` racing a completion in Stripe.
Check that every Stripe write is either keyed by a value committed first or
safe to repeat.
- **Does a billing error path leak?** `verifyWebhook` puts the Stripe SDK's
message into the `BillingError` an adapter may return to the sender; follow
every error `webhook` raises to `diagnostic`, and look for a path that
serializes a body, a header, a secret or a message.
- **Does a billing test pin what its bullet claims, or only what StripeDev
allows?** `packages/stripe/src/testing.ts` is a simplified Stripe; a test
whose assertion holds only because the simulator cannot produce the bad case
leaves its clause unenforced.
- **What do the integration tests not cover?** Name the gaps. A guarantee whose
only evidence is that nobody has attacked it is worth saying out loud.
- **Have `SECURITY.md` and the code drifted?** Say which side is wrong. A bullet
Expand Down
4 changes: 2 additions & 2 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@
"minimumReleaseAge": "14 days"
},
{
"description": "@types/node must not expose APIs newer than the Node major in .nvmrc and CI",
"description": "@types/node must not expose APIs newer than the Node major pinned in devEngines.runtime, which CI reads",
"matchManagers": ["npm"],
"matchPackageNames": ["@types/node"],
"allowedVersions": ">=24 <25"
},
{
"description": "Node is governed by engines, .nvmrc and the CI setup-node version, and is bumped by hand",
"description": "Node is pinned in the root package.json devEngines.runtime, which pnpm and CI both read, and is bumped by hand",
"matchPackageNames": ["node"],
"enabled": false
}
Expand Down
11 changes: 6 additions & 5 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,22 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 26
# The exact pin in `devEngines.runtime`, which pnpm also enforces locally.
node-version-file: package.json
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm format:check
- run: pnpm db:verify
- run: pnpm typecheck
- run: pnpm test:scripts
- run: pnpm test
- run: pnpm --filter @pgstencil/example-workers build
- run: pnpm --filter @pgstencil/example-better-auth build
- run: pnpm packages:verify
- name: Remove this job's containers and volumes
if: always()
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ jobs:

- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 26
node-version-file: package.json
cache: pnpm
registry-url: https://registry.npmjs.org

Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/security-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ jobs:
# on any other branch, or a dispatch from one, never receives the token.
environment:
name: security-audit
# Generous: the audit runs the integration suite against real Postgres
# containers as evidence, then reads the auth package adversarially.
timeout-minutes: 40
# Generous: the audit runs the integration suites against real Postgres
# containers as evidence, then reads the auth and billing packages
# adversarially. Runs took 13 to 23 minutes before billing joined the scope.
# Keep this under the 55 minutes `release.yml` waits for the verdict.
timeout-minutes: 50
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -48,7 +50,7 @@ jobs:

- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 26
node-version-file: package.json
cache: pnpm

- run: pnpm install --frozen-lockfile
Expand Down
1 change: 0 additions & 1 deletion .nvmrc

This file was deleted.

2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ pnpm-lock.yaml
**/dist/**
**/snapshots/**
**/db.generated.ts
examples/login/schema.sql
examples/better-auth/schema.sql

.vscode/
28 changes: 28 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
This codebase is yours: what it's for and how it's built are your call, and you'll keep building on it.

# Working in pgstencil

pgstencil is three npm packages (`packages/pgstencil`, `packages/auth`, `packages/stripe`), one example application (`examples/better-auth`) and the tests that pin them. README.md explains the recipe, PACKAGES.md the public API and release process, BILLING.md billing, WORKERS.md the Workers adapter, SECURITY.md every guarantee and the test that pins it, and CHANGELOG.md what changed.

## Commands

Docker must be running; nothing else needs preparing.

- `pnpm check`: formatting, typecheck, script tests and the whole suite. Run it before calling work done.
- `pnpm test <file>`: one file. The whole suite takes about 20 seconds.
- `pnpm snapshot:update <file>`: the only way snapshots are rewritten. Read the resulting diff.
- `pnpm db:verify`: the committed schema dump and generated types still match the migrations. Rerun `pnpm db:schema` and `pnpm db:types` after touching SQL.
- `pnpm packages:verify`: packs the tarballs and exercises them from an unrelated project. It refuses a modified tree, so commit first.
- `pnpm dev`: the example with a local inbox and Stripe simulator.
- `pnpm db:migration:create <name> --in <dir>`: a new numbered SQL file. Name the package's directory with `--in`; the default is the last source in `pgstencil.json`.

## Rules that are easy to break

- Applied SQL is immutable. Never edit, rename or delete a migration file that has shipped; add a new numbered file. Deployed databases validate every applied file by checksum.
- A change to anything SECURITY.md describes needs its `FAIL IF` rule and pinning test updated in the same change. Tests are cited there by name, so renaming a cited test breaks the audit.
- Time and randomness are injected (`Time`, `RandomSource`), never read from globals, in code that takes them. Better Auth cannot take them, so its tests run a bundle built with `@pgstencil/auth/better-auth-testing`; that module must never reach a production bundle.
- Tooling dependencies (`testcontainers`, `node-pg-migrate`, `cheerio`, `turndown`, `kysely-codegen`) are optional peers loaded dynamically. No runtime entry point may import them statically; `scripts/runtime-deps.test.mjs` enforces it.
- Better Auth is pinned to an exact version because the integration reads its internals. Treat every bump as a change to review, and rerun the auth suites.
- The packages are libraries for other applications. Describe consumers generically in code and docs, and record every breaking change, with how to adapt, under Unreleased in CHANGELOG.md.
- Node is pinned in the root `package.json`'s `devEngines.runtime`; pnpm and CI both read it. Change it there and nowhere else.
- A release is a pull request that raises `version` in all three packages; merging one stages a publish. Do not bump versions for any other reason.
Loading
Loading