Repository navigation
NTNT v0.5.1
NTNT v0.5.1 Release Notes
v0.5.1 adds two opt-in capabilities: a secure passwordless email sign-in flow and provider-neutral opaque secrets. Applications can keep using existing std/env configuration without adopting either feature: get_env, load_env, and .env files retain their behavior in development and production. Existing authentication deployments should review the upgrade notes below for callback-origin and token-lifetime hardening.
Highlights
Magic-link primitives
issue_magic_link(identifier, options?)creates opaque selector/verifier tokens using OS randomness, stores only the verifier hash, and defaults to a 15-minute lifetime with a one-hour maximum.consume_magic_link(token)atomically verifies and consumes a link, returning a safe local-identity payload without granting application roles or creating a session.- Valid-shaped issuance requests remain account-neutral: missing, disabled, and locked identities receive equivalent response shapes without a persisted usable token.
Coordinated magic-link flow
magic_link_flow(req, options)serves the request page, a generic response with best-effort padding, fragment-clearing confirmation page, token consumption, application authorization, and request-aware session completion.- Applications retain narrow
eligible,deliver, and post-consumeauthorizecallbacks;eligiblemust returnboolorResult<bool>so ambiguous policy records cannot imply approval. - Bearer tokens remain in URL fragments until explicit confirmation. Links use an explicit trusted
base_urlorSITE_URL, never request host or forwarded protocol headers. - The coordinator forces provider, subject ID, and canonical email from the consumed identity; application authorization may add session metadata but cannot replace the authenticated principal.
Durable abuse and delivery controls
- Client limits run before eligibility checks; eligible-identity limits run before issuance.
- Counters are atomic across memory, SQLite, PostgreSQL, and Redis/Valkey, use immutable socket-peer identity by default, and store only HMAC-scoped keys, counts, and expirations.
- Failed delivery discards the exact issued token without revoking a newer concurrent token. Delivery budget hints are advisory and timing padding remains best-effort, so production callbacks should enqueue quickly.
Shared one-time-token substrate
- Password resets and magic links share purpose-discriminated storage while remaining impossible to consume across purposes.
- One active magic link per identity, atomic replacement, replay rejection, and active-account checks are enforced across memory, SQLite, PostgreSQL, and Redis/Valkey.
- PostgreSQL keeps identity-first lock ordering; Redis issuance and consumption remain atomic Lua operations and return the identity validated inside the consume script.
Provider-neutral secrets
get_secret(name) -> Option<Secret>andrequire_secret(name) -> Secretprovide opt-in lookup by logical name throughstd/secrets.Secretis an opaque runtime and typechecker value with redacted display/debug behavior and zeroized internal storage. There is intentionally no general Secret-to-String reveal function.- Projects that use
std/secretsand have anntnt.tomldeclare allowed names under[secrets.<NAME>]. Declarations contain metadata only; secret values never belong in the manifest. - The development environment provider performs exact-name lookup. Production Unix deployments can select the generic local secrets-agent provider without coupling ntnt to a particular secrets backend.
Secret transport and sink safety
- Secret values can enter explicit outbound
fetch()sinks: headers, cookies, basic-auth fields, raw bodies, JSON leaves, and form fields. - Secret-bearing requests require HTTPS, do not follow redirects, and allow plaintext HTTP only for direct localhost/loopback development requests when
APP_ENV=development. - Secret values are rejected from caches, database parameters, KV and job payloads, task/channel serialization, templates, public responses, URL query construction, and CSV/JSON serialization.
str(secret), logs, diagnostics, display, and error formatting emit[REDACTED]instead of the secret value. cache_fetch()rejects secret-bearing options because its cache key does not include credential identity.
Generic Unix-socket secrets provider
NTNT_SECRETS_PROVIDER=unix-socketconnects to any local agent implementingdocs/secrets-agent-protocol.md.- One to eight ordered endpoints are supported, with bounded attempts and failover only for transient
unavailableresults. - Production endpoints must live beneath
/run/ntnt-secrets; filesystem ownership and permissions authenticate the local endpoint. - Protocol version, request ID, logical scope, message shape, frame size, value size, and one monotonic attempt deadline are validated before returning a value.
- Raw paths, scopes, backend errors, response fragments, and secret values are omitted from diagnostics.
Upgrade and auth hardening
- SQLite and PostgreSQL migrate legacy password-reset rows transactionally and remove the old table.
- PostgreSQL startup migrations use a transaction-scoped advisory lock so rolling multi-instance upgrades serialize safely.
- Redis/Valkey initialization purges legacy reset-token namespaces that lack an explicit purpose discriminator.
- Public issuance and verification failures no longer expose storage-backend diagnostics.
- Empty-password validation returns a user-facing error without the internal auth prefix.
- Caller-configured password-reset lifetimes are capped at 24 hours; the default remains one hour.
- Password changes and successful resets revoke every outstanding password-reset and magic-link token for the identity across all backends.
- OAuth callback inference now lets
x-forwarded-proto: httpsupgrade a synthetic origin-side HTTP scheme behind TLS-terminating proxies without downgrading an HTTPS request. ExplicitSITE_URLremains authoritative; public plain-HTTP origins without an HTTPS proxy signal and localhost development retain their existing HTTP behavior. - The typechecker now matches the documented
load_env(path) -> Result<Unit, String>contract and accepts typed wrappers that propagate the result. - The typechecker now accepts both documented
cache_fetch(cache, url)andcache_fetch(cache, options)forms.
Compatibility with v0.5.0 applications
std/secrets does not replace std/env and is not activated merely by upgrading the runtime:
- Existing calls to
get_env(...)behave exactly as before. - Existing calls to
load_env(".env")remain supported whenNTNT_ENV=production. NTNT_SECRETS_PROVIDERand the otherNTNT_SECRETS_*variables affect onlyget_secret(...)andrequire_secret(...)lookups.- An application that does not call
std/secretsdoes not need a secrets provider, socket, manifest declaration, or deployment change. - Existing environment variables and
.envfiles are not converted, invalidated, or migrated.
The std/secrets environment provider itself is development-only and fails closed in production. That restriction applies only to the new opaque-secret API; it does not disable ordinary environment-variable configuration through std/env.
Application boundary
The low-level magic-link APIs own token security, durable storage, expiry, replacement, atomic consumption, account-state validation, and sanitized errors. Applications using those primitives directly own delivery, throttling, authorization, confirmation UX, and session creation.
The optional magic_link_flow(...) coordinator owns the common request, confirmation, rate-limit, delivery-cleanup, consume-ordering, and session ceremony. Applications still own eligibility, actual delivery, and post-consume authorization policy.
The optional std/secrets APIs own opaque values, declaration enforcement, provider lookup, sink restrictions, and safe diagnostics. Deployment operators own the selected production secrets agent and its filesystem permissions.
Upgrade notes
- Existing password-reset APIs continue to work through the shared one-time-token substrate.
- Durable SQL backends migrate automatically on initialization. Redis/Valkey legacy reset tokens are invalidated during initialization because their old records cannot prove token purpose.
- Applications that explicitly configured password-reset token lifetimes above 24 hours now receive the 24-hour maximum.
- Set
SITE_URLto the canonical public origin for explicit OAuth callback control. Without it, forwarded HTTPS upgrades an origin-side synthetic HTTP scheme, while public plain-HTTP and localhost requests keep their existing protocol. - No secrets configuration is required unless the application opts into
std/secrets.
What's Changed
- feat(auth): ship purpose-bound magic links in v0.5.1 by @larimonious in #159
- Add coordinated magic-link authentication flow by @larimonious in #160
- feat: add provider-neutral std/secrets by @larimonious in #161
- fix: reject secrets from caches and database parameters by @larimonious in #162
- fix: require HTTPS for secret-bearing requests by @larimonious in #163
- feat: add generic Unix-socket secrets provider by @larimonious in #165
- fix: make v0.5.1 upgrade-safe for existing apps by @larimonious in #166
- test: stabilize oversized socket frame on macOS by @larimonious in #167
- ci: serialize macOS release socket tests by @larimonious in #168
- test: verify macOS release socket framing by @larimonious in #169
Full Changelog: v0.5.0...v0.5.1