Skip to content

v0.5.0 — first-contact remediation (BREAKING auth)

Choose a tag to compare

@DonaldMurillo DonaldMurillo released this 11 Jun 03:41
· 63 commits to main since this release

The first-contact release: an adversarially-verified 10-dimension audit
(2026-06-09) found the engine strong but the first-touch surface broken —
the README quickstart failed verbatim, the flagship example shipped
insecure, RBAC was unreachable from the blueprint, and CI was red on every
release tag. Everything below closes those findings. Contains a BREAKING
auth change (see below); pin a version (go get …@v0.5.0).

Changed

  • BREAKING: battery/auth fails closed on an empty JWTSecret in
    production.
    An AuthManager with DevMode=false and no JWTSecret
    now makes Init return an error (the app refuses to boot) instead of
    warning and continuing — an empty HMAC key yields forgeable,
    restart-unstable JWTs. The error names the remedy (set
    AuthConfig.JWTSecret, or DevMode: true for local HTTP). The
    blueprint path rejects app.auth with dev_mode: false and no
    jwt_secret at gofastr validate/generate time, so a generated app
    can't be built into the broken state. Migration: set
    AuthConfig.JWTSecret from your secret store (most prod apps already
    do); DevMode is unchanged and still mints a per-process secret.

Added

  • Blueprint access: key — per-operation RBAC from gofastr.yml. An
    optional access: map on an entity (read / create / update /
    delete, each a permission string) threads through
    EntityDeclaration.Access into the generated register.go as
    framework.AccessControl{…}, where the existing CRUD chokepoint enforces
    it fail-closed (403). Fully additive: blueprints without the key produce
    byte-identical output. Closes the audit's "one leg of the secure-by-default
    triad is unreachable from the primary declaration format". Also re-exports
    framework.AccessDeclaration for symmetry with EntityDeclaration.

  • gofastr validate <blueprint.yml>. Parse + full blueprint validation
    (including the module/go.mod coherence check, a render pass, and an
    entity-name check that rejects names whose generated Go identifier would
    not compile, e.g. 2fa_tokens) without generating; exit 0/1 with
    agent-friendly file:line + remedy diagnostics. --from, --config, and
    --out on gofastr generate now also accept the space form
    (--from x.yml), which previously silently did nothing.

  • unscoped-pii lint — CLAUDE.md hard rule #6 in the toolchain. Flags
    any auto-exposed entity (CRUD default-on or mcp: true) with PII-shaped
    fields and no owner_field / access / multi_tenant. Enabling
    app.auth alone does not suppress it — the session middleware is
    pass-through for anonymous requests (an adversarial review proved the
    original auth-suppression wrong by anonymously reading user emails from
    a generated example app). Error from gofastr validate, prominent
    warning from gofastr generate, finding in gofastr audit lint.
    Running it against the repo's own examples found and fixed shipped
    exposures in blog, lms, real-estate, portfolio, and
    project-manager — each now demonstrates a scoping pattern (RBAC-gated
    staff rosters, public-catalog reads with gated writes, lead-capture
    forms with open create and gated reads).

  • Executable-README CI gate. cmd/gofastr/readme_quickstart_test.go
    extracts the README's quickstart blocks and runs them for real: blueprint
    → generate → build → boot → GET /posts 200; plus drift gates (no
    "unpublished"/replace-directive guidance anywhere in the embedded docs,
    README blueprint relations must resolve). The audit found this single
    gate would have caught five of its eight confirmed findings.

  • App.OnReady(func(addr string)) — lifecycle hook that fires after
    the listener has actually bound (and is skipped on start failure).
    Generated apps now print their startup banner from it, so a migrate
    failure can no longer print "Server starting" and then exit 1.

  • Trust surface. SECURITY.md (private vulnerability reporting,
    honest v0.x support policy), CONTRIBUTING.md (truthful prereqs: Docker,
    Chrome, the test-isolation rules), and low-ceremony issue templates.

  • Docs: comparison.md and tutorial-blueprint-app.md. An honest
    head-to-head (PocketBase / Encore.go / Wasp / hand-rolled Gin+sqlc,
    weaknesses included) and the missing thesis tutorial (blueprint →
    generate → secure with auth + owner_field + access: → customize in
    plain Go → deploy), every step executed end-to-end before shipping.
    Plus a README for examples/ecommerce.

  • In-package Postgres coverage for framework/crud. The SQL-generation
    core had 66 sqlite-only test files; a focused testcontainers suite now
    exercises the representative paths (filters, sort, offset+COUNT, cursor
    keyset walk, batch rollback, upsert, eager-load include, soft-delete,
    owner+tenant fail-closed) against real Postgres.

  • First tests for framework/owner (14): fail-closed verified at the
    HTTP gate (anonymous → 401 with no extractor), last-call-wins override
    warns, OwnerField-unset is inert, forged client user_id overridden,
    race-checked. Two layer-level fail-open contracts (ApplyOwnerScope,
    InjectOwner without an extractor) are pinned with tests and comments
    documenting that requireScope upstream is the actual boundary.

  • Common-mistakes callouts completed and gated. The docs claimed every
    topic ends with one; 21 of 60 didn't. Real, code-verified callouts added
    to the 15 guide docs (including entity-declarations, "the heart of the
    model"); 6 data/index docs exempted with reasons; the claim text now
    matches reality and TestGuideDocsEndWithCommonMistakes enforces it.

  • Security ledger fully re-verified. All 103 SECURITY_FINDINGS.md rows
    re-checked against current code: 102 fixed (each with the mitigation
    cited AND a named pinning test run and observed passing), 1 accepted
    (#58, an intentional accepted-risk documented in code), 0 unverified or
    open. A guard test pins header-count == row-count and valid status
    tokens (fixed/open/needs-verification/accepted).

  • Coverage floors are a CI gate. scripts/coverage-floors.sh fails the
    blocking job if any claimed package drops ~2 points below its measured
    coverage. COVERAGE_NOTES.md now separates own-package numbers from the
    full-suite-overlay numbers the old 100% claims were quoting.

Fixed

  • SECURITY: EntityConfig.MaxListLimit could be bypassed on two list
    paths.
    The cursor path never consulted the entity cap (asked for ≤3,
    served up to 100), and an oversized ?limit on the offset/stream path
    silently fell back to the default page size (20) — exceeding any cap
    below 20. Both were hidden behind the security tests' auto-skip-on-500
    heuristic; converting those skips to hard failures (an audit
    recommendation) exposed them immediately. Oversized ?limit now clamps
    to the effective cap on every path (listLimitCap shared by offset,
    stream, and cursor), with regression tests un-skipped and green.

  • crud security tests fail instead of skip on server errors. The
    skip-on-redacted-500 heuristic (and its "SQLite can't run $N
    placeholders" rationale, which was false — the 500s were fixtures
    missing Table) is gone across the suite.

  • battery/queue tests are deterministic. Thirteen sleep-based
    assertions replaced with Close-drain semantics, bounded waitFor
    polling, and an unexported clock seam for lease/visibility expiry —
    stable under -race -count=3; no production behavior change.

  • Doc staleness found while verifying the new callouts: widgets.md
    described a widget.Mount return value and bootstrap route that don't
    exist (rewritten around MountRuntime/RuntimeTag);
    ui-new-components.md cited three nonexistent drift gates (corrected to
    the real ones); ui-getting-started.md had a non-compiling
    DBFromContext snippet and listed APIPrefix as roadmap (it shipped).
    cursor-pagination.md now documents the per-entity cap.

  • Boot-time auto-migrate now adds missing columns. AutoMigrate did
    CREATE TABLE IF NOT EXISTS only, while deploy.md claimed
    "create tables, add columns" — adding a field to an existing entity
    broke the next boot. It now reuses the existing schema-diff machinery
    and applies the additive changes only (drops/renames/retypes stay
    behind migrate diff's destructive gate); required new columns are
    added nullable, column adds run before index DDL, and a racing replica
    re-reads live columns on the lock-holding transaction and no-ops.
    Also fixes Postgres live-schema readers to case-fold unquoted table
    names (mixed-case entities were mis-reported as missing every boot).

  • Light color scheme now passes WCAG AA — and the axe gate can no
    longer be platform-blind.
    The "Linux-only" CI axe failures were real:
    Linux Chrome defaults to prefers-color-scheme: light, so CI audited
    the light palette that Dark-mode dev Macs never did. Light primary/
    accent/code-comment tokens retoned (worst offender was 1.71:1, now all
    ≥4.6:1), the framework's DefaultTheme status tones darkened so any
    light-theme Badge/Tag chip passes AA, the gofastr theme init scaffold
    updated to match, and TestAxe_AllPagesAreClean now scans every page
    under BOTH forced schemes. The browser-e2e CI job is blocking again.

  • Generated auth honors dev_mode and validates it strictly. The
    generator hardcoded DevMode: true; the blueprint key now works, with
    a deliberate default of true (production cookie posture —
    __Host-session + Secure — never round-trips on the plain HTTP a
    fresh app serves) announced in the generated code, the gofastr generate output, and the docs. dev_mode: yes is a hard error, not a
    silent coercion to prod mode. auth.CSRF is deliberately not mounted
    (it would 403 the JSON/MCP surface); the gap and the
    SameSite=Strict mitigation are documented.

  • Docs-site copy drift caught by visual review. The site header
    advertised "pre-alpha 0.0.4" and the examples pages described blog as
    "JSON-declared" — a format removed in v0.4.0. Version now lives in one
    siteVersion constant; blog is correctly described as Go-declared
    (users/posts/comments).

  • The README quickstart now runs verbatim — and CI enforces it. The
    blueprint example was rewritten in block style (the in-house YAML parser
    deliberately rejects flow mappings) with every referenced entity
    declared; a long-unclosed code fence that inverted every subsequent
    block is closed; stale "unpublished / add a replace directive" guidance
    is gone (the module resolves on the proxy at v0.4.0); the
    go test ./... claim now states its real prerequisites.

  • Flow mappings fail with an honest error. core/yaml now says
    flow mapping "{ ... }" is not supported; use block style … instead of
    the misleading "nested mapping must be on an indented line"; flow-list
    items that previously silently misparsed now error.

  • Relation-typed fields are validated at generate time. A field like
    author_id: {type: relation, to: users} pointing at an undeclared
    entity now fails gofastr generate/validate with a remedy, instead of
    generating an app that exits 1 at startup. Blueprint module: that
    contradicts the enclosing go.mod errors with the exact fix.

  • Generated apps with auth actually authenticate. The generator
    enabled the auth battery but never mounted auth.SessionMiddleware, so
    authorized requests got 401 like anonymous ones (found by dogfooding the
    secured flagship). Generated app.go now mounts it after
    authMgr.Init; the flagship test asserts the full register → login →
    create → owner-isolated list flow across two users.

  • examples/ecommerce no longer ships insecure. Auth is enabled and
    orders / order_items are owner-scoped (owner_field: user_id) —
    previously any anonymous caller or MCP agent could read every customer's
    PII and mutate orders, violating the repo's own hard rule #6.
    BUILD_JOURNAL.md keeps the honest history.

  • Deterministic CI. core/static MIME detection now consults its
    canonical extension table before the host mime database, so Content-Type
    is identical on macOS and Linux (the 6/6-failing TestDetectFromName is
    fixed at the detection layer, not the test); .js/.mjs serve the
    RFC 9239 canonical text/javascript. ci.yml is restructured into a
    blocking deterministic job and an isolated, serialized browser-e2e job
    (non-blocking until the known Linux-Chrome axe contrast discrepancy in
    examples/site is resolved — condition documented in the workflow).

  • Docs drift purged. overview.md core/ package count corrected
    (twelve → eighteen); framework/ARCHITECTURE.md's layering map redrawn
    to match the real import graph (openapi above crud, the
    slowquery → db edge, crud → access); core-ui/ARCHITECTURE.md now
    states the real runtime size (~7,400 lines across budget-enforced split
    modules, ≤12KB-gz core) instead of "a few hundred lines";
    examples/README.md gained the flagship row.

Changed

  • Example blueprints declare their real module paths.
    blog/lms/portfolio/project-manager/real-estate now declare
    module: github.com/DonaldMurillo/gofastr/examples/<name> (matching the
    flagship), so gofastr validate passes in-place inside the repo.
  • Repo-wide gofmt sweep + CI gate. 299 tracked files reformatted in one
    mechanical pass; the blocking CI job now fails on any gofmt drift in
    tracked Go files.
  • README repositioned around the wedge. Leads with "one blueprint
    becomes a server-rendered UI and an API with secure scopes, in plain Go
    you own"; MCP/OpenAPI demoted to supporting evidence (schema-derived MCP
    became table stakes); validation-status block updated for the secured,
    CI-gated flagship.