Skip to content

ZigBase 0.6.0

Choose a tag to compare

@github-actions github-actions released this 23 Jun 01:06
· 1046 commits to main since this release
Immutable release. Only release title and notes can be modified.
b77f9f3

Features

  • Auth-aware Data.create — Data.create on an auth collection now runs the same credential transforms as the HTTP records handler (generates the per-record tokenKey, forces verified=false, hashes password when supplied), so a programmatically-created record works with zigbase.auth.issueSession / mintLinkToken immediately. password is optional, enabling passwordless (magic-link) sign-up to provision an account without hand-writing credential columns. Non-auth collections are unaffected; the lower-level engine records.create still does a raw insert for imports/migrations.
  • magic_link and otp auth methods now honour auto_create: true — when an unknown identity calls initiate, a passwordless account is provisioned automatically (email set from the identity, verified = false) and the link or code is sent as usual. Enables "sign up or sign in" in one step. Accounts are created with verified = false; pair with require_verified only when a verification flow is in place.
  • CommandMailer (local-command / sendmail mailer) — a built-in mailer that pipes the serialized RFC822 message to a local MTA's stdin (e.g. sendmail -t -i or msmtp -t) and treats exit 0 as success. The standard "delegate delivery to a local relay, hold no SMTP credentials in the app" setup. Selected via the new ZIGBASE_SENDMAIL_COMMAND env var (whitespace-split into argv; From: from ZIGBASE_SMTP_FROM), which takes precedence over SMTP in DefaultMailerPlugin. Re-exported as zigbase.CommandMailer.
  • Comptime .indexes on collection literals — a zigbase.App(.{ .collections = … }) collection may now declare .indexes = .{ .{ .name, .fields, .unique?, .collation?, .where? }, … }, lowered into the provisioned schema and emitted as CREATE INDEX DDL (case-insensitive via .collation = .nocase; conditional-unique via .where). Index .fields reference fields by their declared name.
  • ZIGBASE_PUBLIC_URL → clickable magic-link emails — set public_url (env ZIGBASE_PUBLIC_URL) and the built-in magic_link method emails an absolute link to its consume endpoint (which sets the session cookie and redirects) instead of a bare token. Unset preserves the previous raw-token email. Lets a stock binary offer real magic-link login by configuration alone.
  • Comptime OAuth2 providers: declare .auth.oauth2 = .{ .enabled = true, .providers = .{ .{ .name = "google", .redirectUrls = .{…} } } } on an auth collection in .collections. The runtime clientId/clientSecret are sourced from ZIGBASE_OAUTH_<NAME>_CLIENT_ID / ZIGBASE_OAUTH_<NAME>_CLIENT_SECRET at provisioning time and the secret is encrypted (AES-256-GCM) before it is persisted — secrets never live in the binary. (Applied on first creation only; rotate via the admin API.)
  • Dev-only injectable test clock (ZIGBASE_FAKE_NOW) — freeze the framework's "now" to an ISO-8601 UTC instant (e.g. 2029-03-07T16:00:00Z) so time-boundary scenarios (token expiry, scheduling, challenge/cursor TTLs) are deterministic in e2e suites. Every framework-controlled timestamp routes through one clock seam (src/clock.zig) that honors the override. Gated off in production: compiled in only on a dev_clock build (on in Debug, off in any release build / shipped binary), so a production binary never reads the env var and time can never be frozen. Scope and the production gate are documented in Known limitations → Testing. Closes #58.
  • golfsim example: require_verified = true on the users auth collection — guests must verify their email before a session is minted (booking/payments justification).
  • golfsim example: OTP passwordless login (auto_create = false) for existing verified accounts; first-time onboarding remains password signup + email verification.
  • golfsim example: comptime indexes — NOCASE unique on users.email (prevents case-variant duplicate accounts) and a partial composite index on bookings(listing, starts_at) WHERE status != 'cancelled' (backs the double-booking overlap check and availability route).
  • golfsim example: OAuth2 "Sign in with Google" via comptime .auth.oauth2; client credentials sourced from ZIGBASE_OAUTH_GOOGLE_CLIENT_ID / ZIGBASE_OAUTH_GOOGLE_CLIENT_SECRET at provision time; Google-verified accounts are created verified=true.
  • golfsim frontend: multi-step Auth component covering password sign-in, OTP initiate/complete, signup, email-verification, and Google OAuth2 flows.
  • Blog example: adds built-in magic_link auth on users (passwordless login via
    emailed link, auto_create = true, 1 h TTL, server-redirects to /).
  • Blog example: NOCASE unique comptime index on users.email via .indexes = .{...}
    — prevents case-variant duplicate accounts.
  • examples/plugins showcases the full advanced auth surface: authors auth collection with WebAuthn (passkeys) + a custom ApiTokenMethod plugin; commenters auth collection with magic-link (auto_create=true); onAuth hook logging all three methods; comptime NOCASE collation index on authors.contact_email; frontend magic-link comment flow; beforeCreate hook auto-populating commenter from session.
  • Comptime index collation + partial predicates — schema.Index gains collation (.binary default / .nocase, applied per indexed column) and an optional where: ?[]const u8 partial-index predicate. Case-insensitive indexes (CREATE INDEX ... ("email" COLLATE NOCASE)) and conditional-unique indexes (... WHERE deleted_at IS NULL) are now expressible in the comptime .collections schema and emitted in the generated CREATE INDEX DDL, instead of requiring an out-of-band raw-SQL bootstrap. Defaults preserve existing DDL and JSON round-trip behavior.
  • mintLinkToken opaque bound payload — zigbase.auth.mintLinkToken takes a trailing opts: MintOptions arg whose payload (default "") binds a small opaque string into the single-use token's signed pl claim, returned by verifyLinkToken as claims.pl. Lets a magic-link flow carry tamper-proof bound state (e.g. a post-login redirect target) in the one token instead of an unsigned &next= URL param. Signed, not encrypted — readable-but-tamper-proof; keep it small. Existing call sites add .{}.
  • GET .../auth/magic_link/consume — browser-friendly email-link login — GET /api/collections/:col/auth/magic_link/consume?token=…&redirect=/app verifies and consumes the single-use link token (same replay guard as complete), mints the session through the shared issueSession seam (so onAuth(.magic_link) fires and the zb_auth/zb_csrf cookies are set), honors the require_verified gate, and 302s to the redirect target. Two new per-method magic_link options shape the redirect: redirect_default (fallback path when ?redirect= is absent or rejected; defaults to /) and redirect_allow (allow-list of exact paths or /-suffixed prefixes; an empty list permits any safe relative path).

Fixes

  • Comptime .indexes is no longer silently ignored — the documented .indexes key on collection literals was never lowered by the provisioner; it is now applied.
  • Corrected false claim in examples/plugins migration 0002 comment: provisioned collection columns are human-named (field.name), not id-named. Raw migrations targeting migration-owned tables remain valid; the rationale is now accurate.

Changed

  • Documented the CSRF double-submit contract for cookie sessions — the API reference now spells out that cookie-session clients must echo the readable zb_csrf cookie in the X-CSRF-Token header on unsafe methods (POST/PUT/PATCH/DELETE); GET/HEAD/OPTIONS are exempt. A failed CSRF check makes the request anonymous, so the response status follows the collection's access rules — 403 on a create denial, 404 on an update/delete denial against a protected record (existence-hiding) — not a flat 403. Documentation only; no behavior change.

Performance

  • Trim unused subsystems from the vendored SQLite amalgamation (OMIT_UTF16, OMIT_DECLTYPE, OMIT_DEPRECATED, OMIT_PROGRESS_CALLBACK, OMIT_TRACE, OMIT_SHARED_CACHE, DEFAULT_MEMSTATUS=0). The framework uses only SQLite's UTF-8 prepare/step/bind/column/exec surface, so this is a pure build-cost/size win — a smaller shipped binary and ~10% faster SQLite C compile — with no behavior change. FTS5 is intentionally retained.

Security

  • Server-side open-redirect guard on magic_link consume — the ?redirect= target is validated server-side so consumers never re-implement the guard: only same-origin relative paths are honored. Off-origin, protocol-relative (//host), scheme, CRLF/control-byte, backslash, ./.. path-traversal segments, and still-encoded %2e/%2f/%5c payloads are all rejected and fall back to redirect_default.

Internal

  • Changelog-fragments workflow — changes now add a changelog.d/<slug>.md fragment (with one or more ### <Section> headings) instead of editing CHANGELOG.md, so parallel PRs never conflict on the shared changelog. scripts/assemble-changelog.sh aggregates the fragments per section into a new version block in CHANGELOG.md (and its site/ mirror) at release time (run from scripts/release.sh) and deletes them. See changelog.d/README.md.
  • Corrected the "provisioned columns are named by a stable field id" claim in CLAUDE.md and docs/framework.md: physical SQLite columns use the human field name; the stable field id only matches columns across additive rebuilds.
  • Blog example frontend: new magic-link login form in Editor.tsx (email → initiate
    → "Check your email" state), cookie-session detection via getMe() on mount, and an
    AuthStatus nav island for logged-in display after consume redirect.
  • Blog example README: document ZIGBASE_PUBLIC_URL, the fake blog.test URL, and
    the email index.
  • Cache Zig's local cache dir (ZIG_LOCAL_CACHE_DIR) across CI runs, where the compiled SQLite object actually lives. The previous "global cache" step only persisted toolchain artifacts (compiler_rt/translate-c), so every CI run recompiled the SQLite amalgamation once per zig build invocation (~6×/run: main + each example + the unit job). All builds in a job now share one cached local dir, eliminating those recompiles on warm cache. Corrected the misleading comment on the global-cache step.
  • Use deliberately weak argon2id parameters in test builds only (keyed on builtin.is_test). The unit suite hashes/verifies passwords across ~700 tests; at production cost (interactive_2id, 64 MiB) that KDF work alone was ~25 s of every zig build test. A warm zig build test now runs in ~6 s (was ~32 s). The shipped server binary and the Playwright browser suite (which drives the real binary) are unaffected and keep full-strength params.