Skip to content

NTNT v0.5.1

Choose a tag to compare

@larimonious larimonious released this 25 Jul 17:31
· 254 commits to main since this release
f78bd10

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-consume authorize callbacks; eligible must return bool or Result<bool> so ambiguous policy records cannot imply approval.
  • Bearer tokens remain in URL fragments until explicit confirmation. Links use an explicit trusted base_url or SITE_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> and require_secret(name) -> Secret provide opt-in lookup by logical name through std/secrets.
  • Secret is 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/secrets and have an ntnt.toml declare 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-socket connects to any local agent implementing docs/secrets-agent-protocol.md.
  • One to eight ordered endpoints are supported, with bounded attempts and failover only for transient unavailable results.
  • 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: https upgrade a synthetic origin-side HTTP scheme behind TLS-terminating proxies without downgrading an HTTPS request. Explicit SITE_URL remains 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) and cache_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 when NTNT_ENV=production.
  • NTNT_SECRETS_PROVIDER and the other NTNT_SECRETS_* variables affect only get_secret(...) and require_secret(...) lookups.
  • An application that does not call std/secrets does not need a secrets provider, socket, manifest declaration, or deployment change.
  • Existing environment variables and .env files 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_URL to 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

Full Changelog: v0.5.0...v0.5.1