Batches the six remaining mechanical fixes from security PRD #279 into one branch/PR. Supersedes #281, #283, #284, #285, #286, #287 (closed pointing here). Design finalized 2026-08-04 — recorded in docs/adr/0001-single-origin-trust-list.md and the package CONTEXT.md ("Session & security" terms); detail in this issue's comments.
Design (resolved)
- Production signal:
NODE_ENV === "production", no opt-out. Previews are held to production standards.
- Guard timing: guards throw at factory construction, skipped when
NEXT_PHASE === "phase-production-build" so build-once/inject-env-at-runtime pipelines still build. Fallback if NEXT_PHASE proves unreliable: first-request throw.
- One origin list (ADR-0001): better-auth's
trustedOrigins is the single origin trust list. The proxy derives its CORS allowlist from it; the allowedOrigins option is deleted (no alias). Wildcard entries honored with better-auth's pattern semantics (matcher implemented in-package). This dissolves MEDIUM-1's insecure default and structurally fixes the silent cart-id-persistence 403 from Studio origins.
Checklist
Release
Breaking (allowedOrigins removed; prod guards throw). Lands first in the #384 → #282 → #288 sequence; publish once as 0.2.0 after #288 with one consolidated migration section. No deprecation aliases — no external consumers yet.
Out of scope
HIGH-3 #282 (separate breaking PR, re-scoped there) · #288 README security model (must document the gate + SameSite=Lax, no-CSRF-token composition) · secret splitting · logger abstraction.
Batches the six remaining mechanical fixes from security PRD #279 into one branch/PR. Supersedes #281, #283, #284, #285, #286, #287 (closed pointing here). Design finalized 2026-08-04 — recorded in
docs/adr/0001-single-origin-trust-list.mdand the packageCONTEXT.md("Session & security" terms); detail in this issue's comments.Design (resolved)
NODE_ENV === "production", no opt-out. Previews are held to production standards.NEXT_PHASE === "phase-production-build"so build-once/inject-env-at-runtime pipelines still build. Fallback ifNEXT_PHASEproves unreliable: first-request throw.trustedOriginsis the single origin trust list. The proxy derives its CORS allowlist from it; theallowedOriginsoption is deleted (no alias). Wildcard entries honored with better-auth's pattern semantics (matcher implemented in-package). This dissolves MEDIUM-1's insecure default and structurally fixes the silent cart-id-persistence 403 from Studio origins.Checklist
assertProductionSecret: in production,createEpAuththrows on a missing secret, a known example sentinel (list lives in the package), or <32 chars;checkout.sessionSecretkeeps ≥16. Dev warns instead. Example app: remove both hardcoded fallbacks (lib/ep-auth.ts,lib/checkout-context.ts) — missing env fails loudly. No secret split (JWE + checkout AES continue sharingCHECKOUT_SESSION_SECRET).allowedOrigins+DEFAULT_ALLOWED_ORIGINSfromproxy-routes.ts; derive CORS reflection from the auth instance's resolvedtrustedOrigins, with wildcard matching.cart/server-routes.ts(shared helper, GoCrossOriginProtectionsemantics): GET/HEAD/OPTIONS always pass; unsafe methods pass onSec-Fetch-Site: same-origin/none; cross-site requires an exact/wildcardtrustedOriginsmatch; neither signal present → allow (non-browser); reject =403 { error: "untrusted_origin" }.dispatch_failed→{ error: "dispatch_failed", correlationId }(crypto.randomUUID()), full error toconsole.error; dev keepsmessage. No logger abstraction (observability: feat(plasmic-mcp): observability and logging for MCP server #243).parseCookiesinstead of raw-header substring match; delete the dead RSC cookie-write block in the example catchall.extractEpProviderConfig:hostAllowlistoption (default*.elasticpath.com+localhost/127.0.0.1), threaded throughcreateEpAuth/buildEpCtxfor Self Managed Commerce. Rejection — and the existing regex-miss case — logsconsole.errornaming the rejected host and the fixing option at the point of failure; never silently falls through to thebootstrap-placeholderchain.trustedOriginsincl. wildcard; origin-gate matrix (safe method / same-origin / cross-site trusted / cross-site untrusted / no signals); sanitized prod error body; cookie-parse gate; host-allowlist accept/reject.Release
Breaking (
allowedOriginsremoved; prod guards throw). Lands first in the #384 → #282 → #288 sequence; publish once as 0.2.0 after #288 with one consolidated migration section. No deprecation aliases — no external consumers yet.Out of scope
HIGH-3 #282 (separate breaking PR, re-scoped there) · #288 README security model (must document the gate +
SameSite=Lax, no-CSRF-token composition) · secret splitting · logger abstraction.