ZigBase 0.6.0
·
1046 commits
to main
since this release
Immutable
release. Only release title and notes can be modified.
Features
- Auth-aware
Data.create—Data.createon an auth collection now runs the same credential transforms as the HTTP records handler (generates the per-recordtokenKey, forcesverified=false, hashespasswordwhen supplied), so a programmatically-created record works withzigbase.auth.issueSession/mintLinkTokenimmediately.passwordis optional, enabling passwordless (magic-link) sign-up to provision an account without hand-writing credential columns. Non-auth collections are unaffected; the lower-level enginerecords.createstill does a raw insert for imports/migrations. magic_linkandotpauth methods now honourauto_create: true— when an unknown identity callsinitiate, 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 withverified = false; pair withrequire_verifiedonly 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 -iormsmtp -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 newZIGBASE_SENDMAIL_COMMANDenv var (whitespace-split into argv;From:fromZIGBASE_SMTP_FROM), which takes precedence over SMTP inDefaultMailerPlugin. Re-exported aszigbase.CommandMailer.- Comptime
.indexeson collection literals — azigbase.App(.{ .collections = … })collection may now declare.indexes = .{ .{ .name, .fields, .unique?, .collation?, .where? }, … }, lowered into the provisioned schema and emitted asCREATE INDEXDDL (case-insensitive via.collation = .nocase; conditional-unique via.where). Index.fieldsreference fields by their declared name. ZIGBASE_PUBLIC_URL→ clickable magic-link emails — setpublic_url(envZIGBASE_PUBLIC_URL) and the built-inmagic_linkmethod 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 runtimeclientId/clientSecretare sourced fromZIGBASE_OAUTH_<NAME>_CLIENT_ID/ZIGBASE_OAUTH_<NAME>_CLIENT_SECRETat 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 adev_clockbuild (on inDebug, 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. golfsimexample:require_verified = trueon theusersauth collection — guests must verify their email before a session is minted (booking/payments justification).golfsimexample: OTP passwordless login (auto_create = false) for existing verified accounts; first-time onboarding remains password signup + email verification.golfsimexample: comptime indexes —NOCASEunique onusers.email(prevents case-variant duplicate accounts) and a partial composite index onbookings(listing, starts_at) WHERE status != 'cancelled'(backs the double-booking overlap check and availability route).golfsimexample: OAuth2 "Sign in with Google" via comptime.auth.oauth2; client credentials sourced fromZIGBASE_OAUTH_GOOGLE_CLIENT_ID/ZIGBASE_OAUTH_GOOGLE_CLIENT_SECRETat provision time; Google-verified accounts are createdverified=true.golfsimfrontend: multi-stepAuthcomponent covering password sign-in, OTP initiate/complete, signup, email-verification, and Google OAuth2 flows.- Blog example: adds built-in
magic_linkauth onusers(passwordless login via
emailed link,auto_create = true, 1 h TTL, server-redirects to/). - Blog example:
NOCASEunique comptime index onusers.emailvia.indexes = .{...}
— prevents case-variant duplicate accounts. examples/pluginsshowcases the full advanced auth surface:authorsauth collection with WebAuthn (passkeys) + a customApiTokenMethodplugin;commentersauth collection with magic-link (auto_create=true);onAuthhook logging all three methods; comptimeNOCASEcollation index onauthors.contact_email; frontend magic-link comment flow;beforeCreatehook auto-populatingcommenterfrom session.- Comptime index collation + partial predicates —
schema.Indexgainscollation(.binarydefault /.nocase, applied per indexed column) and an optionalwhere: ?[]const u8partial-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.collectionsschema and emitted in the generatedCREATE INDEXDDL, instead of requiring an out-of-band raw-SQL bootstrap. Defaults preserve existing DDL and JSON round-trip behavior. mintLinkTokenopaque bound payload —zigbase.auth.mintLinkTokentakes a trailingopts: MintOptionsarg whosepayload(default"") binds a small opaque string into the single-use token's signedplclaim, returned byverifyLinkTokenasclaims.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=/appverifies and consumes the single-use link token (same replay guard ascomplete), mints the session through the sharedissueSessionseam (soonAuth(.magic_link)fires and thezb_auth/zb_csrfcookies are set), honors therequire_verifiedgate, and302s to the redirect target. Two new per-methodmagic_linkoptions shape the redirect:redirect_default(fallback path when?redirect=is absent or rejected; defaults to/) andredirect_allow(allow-list of exact paths or/-suffixed prefixes; an empty list permits any safe relative path).
Fixes
- Comptime
.indexesis no longer silently ignored — the documented.indexeskey on collection literals was never lowered by the provisioner; it is now applied. - Corrected false claim in
examples/pluginsmigration 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_csrfcookie in theX-CSRF-Tokenheader on unsafe methods (POST/PUT/PATCH/DELETE);GET/HEAD/OPTIONSare exempt. A failed CSRF check makes the request anonymous, so the response status follows the collection's access rules —403on a create denial,404on an update/delete denial against a protected record (existence-hiding) — not a flat403. 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/%5cpayloads are all rejected and fall back toredirect_default.
Internal
- Changelog-fragments workflow — changes now add a
changelog.d/<slug>.mdfragment (with one or more### <Section>headings) instead of editingCHANGELOG.md, so parallel PRs never conflict on the shared changelog.scripts/assemble-changelog.shaggregates the fragments per section into a new version block inCHANGELOG.md(and itssite/mirror) at release time (run fromscripts/release.sh) and deletes them. Seechangelog.d/README.md. - Corrected the "provisioned columns are named by a stable field id" claim in
CLAUDE.mdanddocs/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 viagetMe()on mount, and an
AuthStatusnav island for logged-in display after consume redirect. - Blog example README: document
ZIGBASE_PUBLIC_URL, the fakeblog.testURL, 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 perzig buildinvocation (~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 everyzig build test. A warmzig build testnow 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.