v3.7.1 — Admin credentials portal (+ critical Beam auth fix)
Manage BeamCheckout and Resend credentials from /admin/settings/secrets instead of wrangler secret put + Cloudflare account access. Encrypted at rest with AES-GCM-256.
⚠️ If you are on v3.7.0, upgrade. That release shipped a Beam adapter that could not authenticate — see Critical fix below. v3.7.0 is marked superseded.
⚙️ Setup: what goes where
This is the section forks need. Two lists.
✅ Manage at /admin/settings/secrets
Set these from the admin UI. super_admin only.
| Key | Required? | What it does |
|---|---|---|
BEAM_MERCHANT_ID |
Yes, for Beam | HTTP Basic username. Beam authenticates as base64(merchantId:apiKey) — a separate credential from the API key. Shown in full (a public identifier, not a secret) so you can verify it against Lighthouse. |
BEAM_API_KEY |
Yes, for Beam | Basic password. Creates charges, issues refunds. |
BEAM_WEBHOOK_SECRET |
Yes, for Beam | Base64 HMAC key verifying X-Beam-Signature. Wrong value = customers charged, orders stuck pending. |
RESEND_API_KEY |
Optional | Order receipts + abandoned-cart email. Unset = email silently disabled, checkout still works. |
All three Beam values are required together — the provider refuses to construct with any missing, so misconfiguration fails at boot rather than at checkout.
Secret fields are write-only: after saving, only a masked ••••••••4a2f preview is shown. Plaintext is never sent back to the browser.
🔒 Still required in the Cloudflare Dashboard
| Key | Why it can't move |
|---|---|
BETTER_AUTH_SECRET |
Read in authHook on every request, before a session exists — storing it behind a session-gated page is circular. It signs session cookies, so read access = forge a login as any user. And it is the key-derivation root encrypting the secrets table, so storing it beside the ciphertext defeats the encryption. |
CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID |
Deploy-time credentials that create the Worker — they cannot live inside it. |
Also Cloudflare-only (bindings and plain config)
Bindings in wrangler.toml: DB (D1), MEDIA_BUCKET (R2), CONTENT_CACHE (KV).
Vars: BETTER_AUTH_URL, PUBLIC_SITE_URL, CMS_SITE_URL, DEFAULT_LOCALE, SUPPORTED_LOCALES, CRON_SECRET, RESEND_FROM, BEAM_BASE_URL (optional — set to https://playground.api.beamcheckout.com for sandbox).
ℹ️
RESEND_FROMstays env-only, so a site can have the API key set in the UI and still not send. Tracked in #116.
🔁 Precedence: env always wins
A key set as a Cloudflare env var takes precedence; the UI shows "Set in Cloudflare" with the field disabled. Deliberate:
- a leaked key can be rotated with
wrangler secret putwithout needing a working admin panel — which may be exactly what's compromised - existing deployments keep working untouched; nothing to migrate
- staging can override production values from a shared database
To manage a key from the UI, remove the env var first.
🚨 Critical fix: Beam authentication
The adapter assumed Beam was "roughly Stripe-shaped". It is not, and four defects meant it could never authenticate against the real API:
| # | We did | Beam requires | Impact |
|---|---|---|---|
| 1 | Authorization: Bearer <apiKey> |
HTTP Basic base64(merchantId:apiKey) |
Every charge and refund rejected |
| 2 | No merchant ID at all | Merchant ID is the Basic username | Cannot authenticate |
| 3 | /v1 in base URL → /v1/charges |
/api/v1/charges on the bare host |
404 |
| 4 | Hex digest, lowercased signature | base64, case-sensitive, key decoded from base64 first | Real webhooks rejected — customers charged, orders stuck pending |
Defect 4 is the worst: the customer pays, Beam confirms, we reject the signature, and the order sits pending forever while the money is gone.
Added 13 contract tests pinning the wire format against Beam's docs — nothing previously asserted headers, paths, or digest encoding, which is why all four were invisible. Mutation-verified: restoring bearer auth fails 1 test; restoring the hex digest fails 4.
Verify against a real sandbox webhook before trusting production traffic. These tests pin the format against documentation, not a live endpoint, and Beam's webhook doc page 404'd on direct fetch during research (the scheme was corroborated across two independent sources).
🚀 Upgrading a fork
git fetch upstream && git merge upstream/main
npx wrangler d1 migrations apply <your-db> --remote # applies 0022_managed_secretsThe migration is required. Without it the page renders and explains itself, but saving fails.
Expect conflicts in wrangler.toml (keep yours — upstream ships placeholders), and possibly .github/workflows/deploy.yml and README.md if customised. Engine files under src/ should take upstream.
BETTER_AUTH_SECRET makes every stored secret undecryptable. Intentional — it fails closed rather than handing a wrong key to a payment provider. The UI reports "Cannot decrypt" and prompts re-entry. A re-key command is tracked in #116.
What else is in v3.7
First tagged release; package.json moves 0.1.0 → 3.7.1 to match the milestone scheme the README already used (shipped through v3.5; v3.6 = registry + spec layer).
- Registry + spec layer (#91 #94 #96 #100) — user-definable content types; typed spec/attribute model with value intervals, qualifiers and polarity; edge attributes and external relation targets
- Phase 5 (#105) — idempotent demo seed scripts, integration tests applying the real migration files
- CI fixes (#102 #103 #106) — nav registry no longer depends on module init order (this took the demo down once), vitest baseline, Paraglide compile in the deploy job, D1 step gated on token scope
Other fixes
- Client-bundle leak (caught pre-merge). Resolving credentials in the shop plugin's
onInitput the secrets service on the browser import graph. SvelteKit's guard blocked the build. Moved tobeam-config.server.ts; verified no crypto reaches the client bundle. Also fixed latent staleness — a provider built at boot would keep using a key you'd since rotated. - D1 upsert switched to raw
ON CONFLICT DO UPDATE; drizzle's builder isn't uniform across versions on D1's sqlite dialect. - macOS duplicate-file guard in
.gitignore. A duplicated migration silently re-runsCREATE TABLE, blocks the chain, and leaves schema tests passing against stale schema.
Tests
103 total, up from 46 at the start of this cycle. Guards for masking, unmanaged keys, and the Beam wire format are all mutation-verified.
Full changelog: f1aef87...v3.7.1