Changelog

Releases

Two distributions, one behaviour: univeros/polaris, the Univeros module, and the polaris/* packages it is built on.

Polaris for Univeros

Rendered from univeros/polaris at v2.4.0 · View source

All notable changes to univeros/polaris are documented in this file. The format follows Keep a Changelog, and the project adheres to Semantic Versioning.

2.4.0 - 2026-10-14

On Polaris for PHP 0.11: polaris/messaging’s sends can leave the request through Univeros Messaging, and polaris:schema:install-plugins uses polaris/pdo’s own createMissing(). Nothing changes for an application that keeps the default outbox; see UPGRADE.md.

Added

  • Univeros\Polaris\Messaging\BusOutbox: polaris/messaging’s outbox on the Univeros Messaging bus. Passed as MessagingPlugin’s outbox and routed to a transport, a send leaves the request as a SendMessage (Receipt::queued(), counted once by the policy) and the worker delivers it through the channels and the fallback; the module registers the handler when univeros/messaging is configured. (#165)

Changed

  • polaris/* move to ^0.11 (Polaris for PHP 0.11). The module implements no messaging Outbox, so its breaking change does not reach the module; the contract and plugin suites replay unchanged.
  • polaris:schema:install-plugins creates the missing tables with polaris/pdo’s SchemaInstaller::createMissing() (and --sql prints them in SqlSchema::ordered()), instead of the foreign-key ordering the module kept of its own. Same output and behaviour. (#111)

2.3.0 - 2026-10-13

Univeros-native extras on univeros/framework 2.6: the Polaris section in the host’s AgentSpec manifests, and audit drains verified with the framework’s WebhookVerifyMiddleware. With this release the module supports every package of Polaris for PHP 0.10.

Added

  • The module contributes its section to Univeros AgentSpec (ManifestContributorInterface, univeros/framework 2.6): bin/altair manifest:generate writes .agent/modules/univeros/polaris.md with every route of core and the configured plugins (auth, step-up, required permissions, error codes) and the permission catalog, byte-stable for --check. (#137)

Changed

  • Requires univeros/* ^2.6.

Documentation

  • README: receiving polaris/audit drains with Univeros WebhookVerifyMiddleware (sha256= signatures, no timestamp header), now that the framework’s signers accept the prefix (#124).

2.2.0 - 2026-10-12

Every polaris/* plugin of Polaris for PHP 0.10 is supported and tested through the module, plus the Univeros-native pieces around them: plugin credentials on the host’s own routes, the agents middleware, the Doctor checks, routes:list, Observability metrics and testing helpers. Nothing changes for an application that configures no plugin.

Added

  • With univeros/introspection, bin/altair routes:list (and routes:show) list the Polaris routes, core’s and the plugins’, beside the host’s, with their endpoint classes. The dispatch table stays without them.
  • With Univeros Observability’s Meter bound and no MetricsInterface of the host’s, Polaris counts its domain events (sign-ins, MFA, the plugins’) through the Meter (MeterMetrics).
  • Univeros\Polaris\Testing\PolarisTesting for the host’s tests (with polaris/testing): bind() gives the container an in-memory database, fresh RS256 keys, an APP_KEY and an issuer; token() mints an access token for a user (organization, roles, MFA, delegated permissions) to call protected routes.
  • The Univeros demo runs the audit, admin, API-keys, OAuth-provider and MCP plugins; its walkthrough adds an API key on the application’s /app/me and an MCP tools/list with a token exchanged for the MCP resource (19 steps).
  • Under POLARIS_PATH_PREFIX, the root .well-known forms of RFC 8414 and RFC 9728 (/.well-known/oauth-authorization-server/auth, /.well-known/oauth-protected-resource/auth/mcp) reach the metadata Polaris serves under the prefix, when that is a Polaris route; any other .well-known path stays the host’s.
  • bin/altair doctor (with univeros/doctor) gains three checks: polaris (secrets, keys, auth settings, database, manifest), polaris_plugins (the plugins build; each plugin’s baseUrl is mounted at POLARIS_PATH_PREFIX) and polaris_schema (the database matches the schema, the plugins’ tables included), each with its fix.
  • With polaris/agents, AgentRequestMiddleware is bound for the host’s routes: a request to a capability’s location must come from an agent with a live grant; other URLs pass through. README: the approval page and the agent SDKs.
  • Every polaris/* plugin of Polaris for PHP 0.10 is supported: passkey, username, passwordless, social, anonymous, multi-session, audit, admin, messaging, sentinel, sso, scim, api-keys, oauth-provider, mcp and agents. Each plugin’s own functional suite and recorded fixtures replay through the module in composer test:plugins (55 tests); no module code change was needed. The README has a recipe per plugin and the registration order.
  • PluginTokenAuthenticationMiddleware (opt-in): the framework’s token authentication plus the plugins’ credentials (API keys, OAuth access tokens) on the host’s own routes, resolved with the request by the plugins’ bearer resolvers, the resolving plugin’s middleware around the route (a key’s own rate limit). Such tokens are delegated; the module binds Polaris\Authorization\Gate, which applies the restriction, for the host’s authorization.

2.1.0 - 2026-10-12

The plugin foundation: any polaris/* plugin of Polaris for PHP 0.10 runs in a Univeros application by configuration (Univeros\Polaris\Plugins or POLARIS_PLUGINS), with its tables, commands and contract fixtures. With no plugin configured the HTTP contract and the schema are unchanged; see UPGRADE.md for the one listener change.

Added

  • The configured plugins’ console commands join bin/altair as polaris:* (polaris:audit:prune, polaris:mcp, …), and polaris:manifest, schema:export, schema:create and doctor include the plugins (the module points their --bootstrap at the application’s Polaris). With plugins configured bin/altair needs the secrets.
  • polaris:schema:install-plugins: creates the tables the database lacks (the configured plugins’ included) in foreign-key order and seeds the plugins’ permissions; idempotent. --sql prints the statements for a host’s own migration. polaris:schema:create, schema:diff and schema:drop include the configured plugins’ tables. The install migration stays core-only: Cycle builds it without the container.
  • Plugins: the polaris plugins an application runs are a bound Univeros\Polaris\Plugins (new Plugins(new PasskeyPlugin(...)) in config/container.php), else the comma-separated class names in POLARIS_PLUGINS, built through the container. Their routes, tables, services, middleware and permissions join the module’s. An entry that is not a plugin class fails boot naming it. With neither, nothing changes.
  • composer test:plugins (part of qa and CI) replays each supported plugin’s own functional suite and recorded fixtures through the module, as test:contract does for core’s. First plugin: polaris/username.

Changed

  • With a bound PSR-14 dispatcher the Polaris listeners (the audit log, notifications, metrics, and every plugin’s) no longer depend on the host subscribing them: the module runs them, then hands the event to the host’s dispatcher. POLARIS_LISTENERS=host keeps 2.0’s behaviour; see UPGRADE.md.
  • polaris/core, polaris/pdo, polaris/psr15, polaris/cli and polaris/testing move to ^0.10. No behaviour change: core’s 52 routes and the 184 contract fixtures are unchanged from 0.1 and replay green through the module.

2.0.0 - 2026-09-11

univeros/polaris 2.0 is a new major built on Polaris for PHP: polaris/core carries the identity, MFA/OTP, session, organization and RBAC services, the 52 endpoints and the schema, all contract-frozen against 1.0 (184 recorded request/response sequences replay through this module’s Relay pipeline in CI); this package is the Univeros module around it. The HTTP contract of 1.0 is unchanged (docs/auth/api-reference.md in polaris-core), with one documented exception: a request body field named like a request attribute no longer overrides the attribute.

Changed

  • The module builds Polaris from the environment (APP_KEY, AUTH_JWT_*, AUTH_ISSUER, AUTH_AUDIENCE) on the application’s database settings (DB_*, or POLARIS_DSN without an ORM) through polaris/pdo; the framework’s cache, logger and event dispatcher are used when bound.
  • Routes: PolarisMiddleware, contributed through MiddlewareProviderInterface ahead of the framework’s exception handler, serves every Polaris path; the routes are no longer in the FastRoute table (bin/altair polaris:manifest lists them). POLARIS_PATH_PREFIX mounts them under a prefix.
  • Migrations: one Cycle migration installs the schema and seeds the permission catalog through Polaris\Pdo\SchemaInstaller; the 18 migrations of 1.x are gone. bin/altair db:migrate on a fresh database installs Polaris.
  • The framework’s TokenAuthenticationMiddleware keeps working with Polaris access tokens through TokenFactoryBridge; the application scopes it to its protected paths with $container->make(TokenAuthenticationMiddleware::class, ['rules' => [...]]).
  • Console: polaris:schema:export, polaris:schema:create, polaris:schema:drop, polaris:schema:diff, polaris:manifest, polaris:doctor from polaris/cli, added by the application’s own bin/altair through Univeros\Polaris\Console\Commands::all().
  • The default token issuer without AUTH_ISSUER is polaris (1.x minted univeros/polaris).

Removed

  • Every class under Univeros\Polaris\ except the module glue; use the Polaris\ namespaces of polaris/core. Cycle entities and repositories: Polaris no longer maps entities into the application’s ORM schema.
  • The AES-CBC encrypter of 1.x. There is no compatibility path (see UPGRADE).

1.0.0 - 2026-06-11

First stable release: the authentication, MFA/OTP, and multi-tenant RBAC module for the univeros framework. A host registers one Module class and gets the full surface below as routes, entities, migrations, middleware, and PSR-14 events. See docs/auth/ for the specification and docs/auth/api-reference.md for the implemented HTTP contracts (52 endpoints).

Identity and sessions

  • Registration with email verification (single-use hashed tokens, enumeration-safe responses) and resend.
  • Password login with Argon2id hashing, transparent rehash, timing-equalized verification, sliding-window lockout, and optional verified-email and HIBP breach-check (k-anonymity) gates.
  • JWT access tokens (RSA, JWKS endpoint, key-rotation overlap window) plus opaque rotating refresh tokens with family-based reuse detection: replaying a rotated token revokes the whole session family.
  • Session management: device list, logout, logout-all, per-session revocation, and an optional instant access-token denylist.
  • Password forgot/reset/change with logout-everywhere semantics; reset and verification tokens are stored only as keyed HMACs.

MFA

  • TOTP (RFC 6238, encrypted secrets, QR provisioning), SMS, and email factors with enrollment + confirmation flows; ten single-use recovery codes issued on the first confirmed factor.
  • The login MFA gate: a short-lived single-purpose ticket bridges the password step to factor verification before any session is minted.
  • Step-up re-authentication for sensitive operations, stamped via auth_time; mfa/amr/auth_time persist across refresh and org switching.
  • OTP hygiene: codes stored as keyed HMACs, attempt budgets and consumption enforced by atomic conditional updates, send quotas and resend cooldowns against OTP bombing.

Multi-tenant RBAC

  • Organizations with soft delete, slug uniqueness, and per-org role templates (owner / admin / member) cloned from system templates; a global superadmin override for platform operators.
  • Members: listing (with PII gating of invited/suspended emails), role assignment, suspension with immediate org-scoped session revocation, and removal.
  • Single-use, expiring invitations bound to the invitee’s email.
  • Custom roles with a 12-key permission catalog; database-resolved authorization on every check (token claims are never trusted for authority).
  • Tenant invariants enforced beyond the permission check: no privilege escalation, owners protected from non-owners, last-owner protection, cross-tenant isolation on every org route.
  • User administration: read/update (self or admin), disable/enable, and anonymizing tombstone deletion.

Hardening and operations

  • Per-IP rate-limit budgets per endpoint group plus a global per-user budget across authenticated endpoints; user-agent sanitization at the edge.
  • Two security audits (#44 sign-off and the #97 follow-ups) fully remediated: atomic rotation/OTP/recovery claims, APP_KEY minimum length, full-length key fingerprints, typed challenge purposes, abuse caps.
  • Append-only audit log (actor, org, ip, user agent, whitelisted metadata), domain metrics counter, and notification listeners over a catalog of ~35 PSR-14 events.
  • 18 driver-portable migrations, scheduled pruning of expired transient rows, and a key-rotation runbook.
  • Verified by 519 tests (unit, persistence against a real driver, and end-to-end functional tests over the real middleware pipeline), with phpcs (PSR-12) and phpstan level 5 clean.

Documentation and agent experience

  • Full specification under docs/auth/ with the API reference and event catalog regenerated from the shipped code.
  • One YAML spec per implemented endpoint under api/, verified 1:1 against the route table.
  • An agent skill at .ai/skills/polaris/SKILL.md covering registration, configuration, the token model, the permission catalog, and the tenant invariants.

Polaris for PHP

Rendered from univeros/polaris-core at v0.11.0 · View source

All notable changes to Polaris for PHP (the polaris/* packages) are documented in this file. The format follows Keep a Changelog, and the project adheres to Semantic Versioning.

[0.11.0] - 2026-10-13

The polaris/messaging Outbox change below is breaking for an Outbox implementation, so the version line moves to 0.11 (^0.11 between siblings). Program 4’s WP5 moves to 0.12.0.

Changed

  • polaris/messaging: the Outbox seam takes data instead of a closure, so a host can back it with a real queue (#54). Outbox::push(Message $message, array $alternatives): ?Receipt receives the message and the alternative recipients of its fallback, both serializable; SyncOutbox (the default) delivers at once and answers the channel’s receipt. The new Sender::dispatch(Message, array $alternatives = []) delivers through the channels and the fallback without asking the policy again or using the outbox: SyncOutbox and a queue’s worker call it. A deferred send answers Receipt::queued() (Receipt::$queued), so a caller tells it apart from a refused send (null); messaging:send prints it as queued. Upgrading: an Outbox implementation changes its signature from push(Message, callable $deliver): void to push(Message, array $alternatives): ?Receipt, and its worker calls Sender::dispatch() instead of the closure. SyncOutbox now takes a Closure(): Sender (the plugin builds it; construct it yourself only when you build a Sender by hand).

[0.10.1] - 2026-10-13

Added

  • polaris/pdo: SchemaInstaller::createMissing() creates only the tables a database lacks (plugins’ included), in foreign-key order, then seeds the permission catalog and the system roles; it answers the tables it created. SqlSchema::ordered() is public. For a host that adds a plugin to an installed database (univeros/polaris#111).

[0.10.0] - 2026-10-12

Program 4 (“Better Auth for PHP”, docs/better/spec.md), WP4: polaris/mcp, polaris/agents and the agent SDKs polaris/agent and @polaris-auth/agent. No core change; core’s 52 routes are unchanged. The version line moves to 0.10 (^0.10 between siblings).

Added

  • polaris/mcp: McpProtectedResource, a PSR-15 middleware for any PHP MCP server that verifies Polaris-issued access tokens locally (signature by kid, issuer, audience, expiry, DPoP binding) against core’s keys or a remote issuer’s JWKS, answers 401 with WWW-Authenticate: Bearer resource_metadata= and 403 insufficient_scope with the missing scopes, and serves RFC 9728 metadata. Polaris as an MCP server (on mcp/sdk): one tool per route the caller may call (named by its spec, polaris.orgs.members.list; schemas from the manifest), dispatched through the pipeline so a tool answers what the route answers; the read-only CLI commands as tools over stdio; polaris://manifest, polaris://schema, polaris://problems/<plugin>/<name>; POST /mcp (2026-07-28 profile, legacy: allow for the 2025 handshake), GET /.well-known/oauth-protected-resource/mcp, bin/polaris mcp (stdio).
  • polaris/agents: AI agents as principals. POST /agents/register and host enrolment (POST /agents/hosts, POST /agents/enroll); each agent is an OAuth client of polaris/oauth-provider (client credentials or private_key_jwt for autonomous agents, token exchange with act for delegated ones) and may sign request JWTs (agent+jwt, aud = the URL, jti once); its tokens carry the permissions of its live grants, so core’s authorization and the delegation rule apply. Capabilities from configuration; grants with constraints and budgets (POST /agents/grants/request), approvals by device code, CIBA or in-app with a strength per risk tier and auto-approval (GET /agents/approvals, POST /agents/approvals/{id}/decide); execution (POST /agents/execute, or AgentRequestMiddleware in front of a host route for a capability with a location) with a signed receipt per execution in a hash chain per agent (GET /agents/receipts/{id}/verify); suspend, resume and revoke (the kill switch seals the chain); GET /agents/session, GET /agents/capabilities, GET /.well-known/agent-configuration (docs/better/agent-auth-compat.md), operator routes under /admin/agents; every capability is an MCP tool. Five tables polaris_agent*; the agent.* audit names, recorded with actor_type = agent.
  • polaris/agent and @polaris-auth/agent (new): the agent SDKs, PHP (any PSR-18 client) and TypeScript (Node and edge, no dependency): discover, register or enrol, request grants and wait for approval, sign request JWTs and private_key_jwt assertions, client credentials and token exchange, execute, verify receipts and their chain; route tables generated from the manifest.
  • @polaris-auth/client: client.mcp and client.agents.
  • The Slim demo: the oauth, mcp and agents plugins, approve.html (the agent approval page), and walkthrough steps for an MCP tools/list and an agent’s enrol-to-receipt run.

Changed

  • polaris/oauth-provider: the token exchange’s act names the requesting client by sub as well as client_id (RFC 8693 §4.1); OAuthPlugin(audiences: [...]) accepts further audiences naming this server (<baseUrl>/mcp); Tokens::revokeForClient().

[0.9.0] - 2026-10-10

Program 4 (“Better Auth for PHP”, docs/better/spec.md), WP3: polaris/api-keys and polaris/oauth-provider, with two core seams (the bearer resolvers and the delegated authority, decisions #2 and #3). Core’s 52 routes are unchanged. The version line moves to 0.9 (^0.9 between siblings).

Added

  • polaris/core: the bearer-resolver seam (program 4, decision #2): Polaris\Contract\BearerResolver and BearerResolverProvider; a plugin’s resolvers authenticate their own credential on core’s auth: bearer routes before the JWT parse (Graph::bearerResolvers(), consulted by psr15’s TokenAuthenticationMiddleware). The delegated authority (decision #3): a token carrying delegated makes Gate::authority() intersect the owner’s permissions with that list.
  • polaris/api-keys: keys owned by users and organizations (pk_live_, pk_test_), each with a subset of the owner’s permissions, an optional rate limit, expiry, rotation with a grace window and revocation; GET|POST /api-keys, GET|PATCH|DELETE /api-keys/{id}, POST /api-keys/{id}/rotate, POST /api-keys/verify; a key in Authorization: Bearer or x-api-key authenticates every Polaris route as its owner, step-up routes excepted. Table polaris_api_key; the api_keys.* audit names.
  • polaris/oauth-provider (plugin id oauth): Polaris as an OAuth 2.1 and OpenID Connect provider. GET /oauth2/authorize (PKCE S256 required; a browser goes to the host’s consent page, an XHR client gets JSON), POST /oauth2/authorize/decision, POST /oauth2/token (authorization_code, refresh_token with rotation and family revocation, client_credentials, device_code, CIBA, token exchange with act; client_secret_basic, client_secret_post, private_key_jwt, none; DPoP-bound tokens with cnf.jkt), GET|POST /oauth2/userinfo, POST /oauth2/revoke, POST /oauth2/introspect, POST /oauth2/register (RFC 7591, off by default), the device flow (/oauth2/device/code, verify, approve), CIBA (/oauth2/ciba, pending, {id}/decide), the consents (/oauth2/consents), /.well-known/openid-configuration and /.well-known/oauth-authorization-server, the organizations’ clients (/orgs/{id}/oauth/clients) and the operators’ (/admin/oauth/clients); client ID metadata documents (https client ids) fetched, validated and cached. Access tokens are RFC 9068 JWTs signed with core’s key, verified by /auth/jwks unchanged, and accepted on every Polaris route within their scopes. Six tables polaris_oauth_*; the oauth.* audit names.
  • @polaris-auth/client: client.apiKeys and client.oauth.
  • The Slim demo: consent.html and device.html, the reference consent and device pages.

Changed

  • polaris/core: the session parser refuses a JWT whose header typ is not JWT, and a token naming an audience when none is configured (the provider’s access and ID tokens are signed with core’s key); a delegated credential (an API key, an OAuth access token) is refused by the authorization middleware on a write or destructive route that needs no permission and on every step-up route, and a delegated authority drops the superadmin role.
  • Core’s test normaliser masks the two packages’ per-run values (hint, client_id, client_secret, user_code, device_code, request, id_token, auth_req_id; the request and user_code query parameters; integer exp, iat, nbf, auth_time, updated_at).

[0.8.0] - 2026-10-09

Program 4 (“Better Auth for PHP”, docs/better/spec.md), WP2: social sign-in and passkeys, polaris/social and polaris/passkey, and the one planned core seam, the MFA factor-type registration. Core’s 52 routes are unchanged. The version line moves to 0.8 (^0.8 between siblings).

Added

  • polaris/social: OAuth 2.0 / OpenID Connect sign-in and sign-up through a catalog of nineteen providers (Google, Apple, Microsoft, GitHub, GitLab, Discord, Facebook, X, LinkedIn, Slack, Twitch, Spotify, Zoom, Notion, Dropbox, Reddit, Kick, TikTok, Hugging Face) or any other OAuth 2 server (a Definition, or OpenID Connect discovery); POST /social/{provider}/start, the GET/POST callback with the sso hand-off pattern and POST /social/exchange, POST /social/google/one-tap (the id_token verified offline), GET /social/accounts, POST /social/{provider}/link (step-up), DELETE /social/{provider} (refused for the last credential), POST /social/{provider}/token (refreshed when expiring); the linking policy (trustedProviders, allowDifferentEmails); the OAuth proxy (proxy) forwarding a signed return_to from preview origins. Table polaris_social_account; the social.* audit names.
  • polaris/passkey: WebAuthn through web-auth/webauthn-lib; POST /passkey/register/options and /verify from a session, POST /passkey/authenticate/options (discoverable, conditional UI) and /verify ending in core’s envelope with amr: ["passkey"] (mfa: true and the gate skipped on user verification), GET /passkey/list, PATCH /passkey/{id}, DELETE /passkey/{id} (step-up). Every passkey is also a core MFA factor of type passkey (mfaFactor: true), verified through core’s /auth/mfa/verify and /auth/mfa/step-up with the assertion as the code. Table polaris_passkey.
  • polaris/core: the MFA factor-type seam (program 4, decision #1): Polaris\Contract\MfaFactorType and MfaFactorTypeProvider; a plugin implementing the provider adds types MfaChallengeVerifier dispatches to, resolved on the first verification. Core’s routes, fixtures and types are unchanged.
  • @polaris-auth/client: client.social and client.passkey, and the browser helpers registerPasskey(), signInWithPasskey() and passkeyAssertion() over navigator.credentials.
  • The Slim demo: a fake OAuth provider (/fake-oauth/*) and a passkey page, exercised by the walkthrough and a Playwright run with a virtual authenticator.

Changed

  • Core’s test normaliser masks the per-run query parameters of a URL (state, nonce, code_challenge, code) in bodies and Location headers.

[0.7.0] - 2026-10-09

Program 4 (“Better Auth for PHP”, docs/better/spec.md), WP1: four sign-in packages on the plugin runtime, polaris/passwordless, polaris/username, polaris/anonymous and polaris/multi-session. Core’s 52 routes are unchanged. The version line moves to 0.7 (^0.7 between siblings).

Added

  • polaris/passwordless: magic links (/magic-link/send, /magic-link/verify redirecting with a one-minute code, /magic-link/exchange), email one-time codes to sign in, verify the email or reset the password (/email-otp/*), phone codes for a verified second contact (/phone/add, /phone/confirm, /phone/send, /phone/verify) and one-time tokens for session transfer (/one-time-token/generate, /verify). Each ends in core’s login envelope with its own amr (magic_link, email_otp, phone; a one-time token inherits the session’s), respects core’s MFA gate, spends every secret once and answers a known and an unknown identifier alike. Sends go through polaris/messaging. Tables polaris_passwordless_secret, polaris_passwordless_phone.
  • polaris/username: PATCH /username and POST /username/sign-in (a username or an email through core’s LoginService); usernames are unique without regard to case on PostgreSQL, MySQL and SQLite (a lowercased unique column), with rules from configuration. Table polaris_username.
  • polaris/anonymous: POST /anonymous/sign-in (a guest, amr: ["anonymous"]), POST /anonymous/convert (the host’s onConvert(guestId, userId), then the guest’s sessions end and it is disabled) and polaris anonymous:prune. Table polaris_anonymous.
  • polaris/multi-session: a middleware that records every token pair against a server-minted device (X-Polaris-Device header, HttpOnly cookie polaris_ms_device), GET /multi-session/list, POST /multi-session/switch (a fresh session with the stored authentication facts, no re-authentication), DELETE /multi-session/{sessionId}, GET /multi-session/last-method. Table polaris_multi_session_device.
  • PasswordlessPlugin::SENTINEL_ROUTES, UsernamePlugin::SENTINEL_ROUTES and AnonymousPlugin::SENTINEL_ROUTES, for polaris/sentinel’s routes option.
  • @polaris-auth/client: client.passwordless, client.username, client.anonymous and client.multiSession; the namespace generator camel-cases a plugin id.

Changed

  • @polaris-auth/client is published to npm by .github/workflows/npm-publish.yml on every v* tag, through npm’s trusted publishing (OIDC, provenance), after the first publish of 0.6.1 by hand; repository.url carries the git+https:// form npm normalizes it to.

[0.6.1] - 2026-09-12

The TypeScript client answers a polaris/sentinel captcha challenge with one retry; the root README lists the six packages of the infrastructure program.

Added

  • @polaris-auth/client: the transparent challenge retry. createClient({ challenge: { captcha } }) takes a callback that yields a captcha token; when a request answers 403 with the sentinel/challenge_required problem and challenge: captcha, the client asks it once and sends the same request again with captcha_token added to the body. Without the callback the problem document is returned as before. challengeRetry(options) is exported as an openapi-fetch middleware.

[0.6.0] - 2026-09-12

polaris/scim, a SCIM 2.0 server per organization; the six packages of the infrastructure program are released.

Added

  • polaris/scim (Polaris\Scim\): a SCIM 2.0 server per organization at /scim/v2/{connectionId} (ServiceProviderConfig, Schemas, ResourceTypes, Users and Groups with filtering eq|co|sw joined by and, PatchOp, 1-based pagination; SCIM error documents that also carry error and message): a directory provisions the organization’s members (created verified without a password, joined with the member role; deactivated, or anonymised when the connection says so, on deprovisioning; only the memberships the connection created are removed) and roles (Groups; the built-in ones take members but keep their names). Connections with a pst_ token stored as a keyed hash, created, rotated and decommissioned by the organization under /orgs/{id}/scim/connections (org.update), listed and removed by the operators under /admin/scim; every action a scim.* audit event.
  • @polaris-auth/client: client.scim.*.

Changed

  • A 204 response carries no content type (a host’s HTTP foundation drops one on an empty response).
  • The Yii test application parses application/scim+json bodies as JSON; a Yii host adds the parser the same way.
  • The packages’ version line is 0.6: ^0.6 between siblings, 0.6.x-dev on the path repository and in the demos.

[0.5.0] - 2026-09-12

polaris/sso, per-organization SAML and OIDC providers with verified domains and single logout.

Added

  • polaris/sso (Polaris\Sso\): per-organization SAML 2.0 (onelogin/php-saml, strict: signatures, one assertion, audience, destination, drift, InResponseTo, replay) and OpenID Connect (discovery, PKCE, nonce, the id_token against the JWKS through firebase/php-jwt) providers; verified domains (a DNS TXT record or a well-known file) route an email to its provider; just-in-time users and memberships with the provider’s default roles; the sign-in ends on the application’s redirect_uri with a one-time code that POST /sso/exchange turns into the login envelope, the session scoped to the organization (amr: sso); single logout both ways (signed IdP logout requests, POST /sso/logout for the application); the SP metadata; the organization’s self-service routes under /orgs/{id}/sso (org.update), the operators’ under /admin/sso; every action and rejection an sso.* audit event.
  • @polaris-auth/client: client.sso.*.

Changed

  • Result::$raw: an endpoint may answer a body as is with its own Content-Type (the SAML metadata XML); the JSON responder writes it unchanged.
  • The OpenAPI document types a nullable field (?integer, ?boolean, ?list<string>) as its type and an object field as an object, so the generated client checks the bodies of the plugin routes; core’s permission_keys is typed as an array now.
  • The packages’ version line is 0.5: ^0.5 between siblings, 0.5.x-dev on the path repository and in the demos.

[0.4.0] - 2026-09-11

polaris/sentinel, the risk engine in front of the guarded auth routes.

Added

  • polaris/sentinel (Polaris\Sentinel\): a local risk engine on sign-up, sign-in, password reset and code sends, as PSR-15 middleware in front of the guarded routes: velocity, credential stuffing, IP rules, devices and disposable domains out of the box, captcha (Turnstile, hCaptcha), impossible travel (MaxMind DB) and breached passwords (HIBP) with a provider; a policy that allows, challenges or blocks (sum capped at 100; allow below 40, challenge from 40, block from 80; a verified captcha token turns a challenge into an allow); observe records without enforcing, enforce answers sentinel/blocked and, with a captcha verifier, sentinel/challenge_required (challenge: captcha, the client retries with captcha_token); every decision where a signal spoke is a row in polaris_sentinel_decision and a sentinel.evaluated audit event; the polaris_device cookie; GET /admin/sentinel/decisions, GET|POST /admin/sentinel/ip-rules, DELETE /admin/sentinel/ip-rules/{id} and POST /admin/sentinel/unblock for the admin plugin’s principals; sentinel:lists; the bundled disposable-domain list (CC0). The breach checker also serves core’s password-policy port; polaris/messaging takes its quiet mode.
  • @polaris-auth/client: client.sentinel.*.

Changed

  • Graph::port() is public: a package takes another package’s optional contribution the way the graph takes a plugin-provided port (polaris/messaging takes a Suppressor from the graph when the host passes none).
  • The packages’ version line is 0.4: ^0.4 between siblings, 0.4.x-dev on the path repository and in the demos.

[0.3.0] - 2026-09-11

polaris/messaging, the client namespaces for the plugins, and the port rule that lets a plugin serve core.

Changed

  • The packages’ version line is 0.3: ^0.3 between siblings, 0.3.x-dev on the path repository and in the demos (a package at 0.3.0 requires its siblings at 0.3.0).

Added

  • @polaris-auth/client: the plugins’ routes as namespaces, client.audit.* and client.admin.*, one typed method per endpoint, generated from the OpenAPI document (x-polaris-plugin); ProblemBody types their RFC 9457 errors. The OpenAPI document marks a plugin’s operations, answers their errors as application/problem+json (Problem schema), keeps operation ids unique across plugins and types list<x> fields as arrays. EndpointSpec::$plugin names the plugin a route belongs to.
  • The Slim demo registers the audit and admin plugins; bin/setup writes an owner API key to var/admin.key, which the client’s smoke test uses.
  • polaris/messaging (Polaris\Messaging\): templated, translated (en, es, de, fr, pt), rate-limited email and SMS through pluggable channels (Symfony Mailer, PHPMailer, Twilio, Vonage, log, array), with instance and per-organization template overrides (polaris_messaging_template), a fallback kind, quiet mode through a Suppressor, an Outbox queue seam, messaging:send; it becomes core’s mailer and SMS sender when the configuration leaves them unset, and every delivery is messaging.sent in the audit store.
  • A plugin may provide a core port (the mailer, the SMS sender, the breach check, the metrics) through services() keyed by the contract; the graph takes it when the configuration leaves the port unset.

[0.2.0] - 2026-09-11

The plugin runtime and the first two packages on it: polaris/audit and polaris/admin. One version line for every package.

Added

  • The plugin runtime: Polaris\Contract\Plugin and Polaris\Plugin\AbstractPlugin, Config::$plugins; a plugin’s models join the schema (export, create, diff, install), its api/<id>/**/*.yaml specs join the manifest and the router, its services resolve endpoint constructors, its listeners join Polaris::listeners(), its permissions the catalog; Polaris::plugin(), Graph::get(). Endpoint::problem() answers RFC 9457 problem documents (application/problem+json) for plugin routes. The CLI commands take --bootstrap (POLARIS_BOOTSTRAP) to see an application’s plugins; the adapters take plugins in their configuration. docs/plugins/README.md documents the contract.
  • Contract fixtures can be recorded (POLARIS_RECORD_FIXTURES=1) into a package’s own directory, so a plugin proves its routes through every harness like core.
  • polaris/audit (Polaris\Audit\): a catalogued, redacted, append-only audit store built on the plugin runtime: core’s events recorded with actor, subject, organization and client context; GET /audit/me, /audit/organization/{id} (with audit.read) and /audit/types, cursor-paginated; sinks (database, PSR-3, JSON lines, signed webhooks) and per-organization drains; retention by policy (audit:prune), an optional hash chain (audit:verify); last_active_at per user.
  • polaris/cli: Polaris\Cli\CommandProvider; bin/polaris adds a plugin’s commands when POLARIS_BOOTSTRAP names the application.
  • polaris/admin (Polaris\Admin\): the operator API under /admin for admin users (a grant, or the superadmin role) and API keys (pak_..., hashed, with IP allowlist and expiry), with the viewer/support/admin/owner role matrix and instance or organization scopes: users (list, read, ban, unban, set password, delete, impersonate), sessions, MFA factors (remove, reset), organizations (list, read with members, member roles, delete), the instance-wide audit trail and the organizations’ drains, statistics, keys and grants; admin:key and admin:grant; every action recorded as admin.* through polaris/audit.
  • Polaris\Contract\Plugin::middleware(Graph): PSR-15 middleware a plugin adds to the pipeline, right after the bearer token is parsed. AccessTokenClaims::$extra and TokenService::mint() mint an access token with a package’s claims and no session; UserAdminService::anonymize() is the erasure without the self-or-permission check, for an operator whose authority is established.

0.1.1 - 2026-09-11

Added

  • A LICENSE file (MIT, copyright 2am.tech) in every package and at the root; authors (2am.tech) in every composer.json and in the client’s package.json; every README ends with the 2am.tech attribution.

Removed

  • Six local artefacts of an earlier demo run committed by mistake under examples/univeros (a demo key pair, .env, the mailbox, a SQLite file, a server log); they carried no real secret and remain only in the history of v0.1.0.

0.1.0 - 2026-09-10

The first release of Polaris for PHP: the framework-agnostic packages extracted from univeros/polaris 1.0.0 with every response contract-frozen, the Laravel, Symfony and Yii adapters, and the TypeScript client. One version line for all of them.

Framework adapters

  • polaris/laravel (Polaris\Laravel\): a service provider that builds Polaris from config/polaris.php on Laravel’s connection, cache, logger, events and mailer; the 52 endpoints as named Laravel routes under path_prefix; the polaris guard (auth:polaris) for the application’s own routes; polaris:install (config and the migration that creates the tables and seeds the catalog) and the CLI commands as polaris:schema:export, polaris:schema:diff, polaris:manifest, polaris:doctor; a plain-text mail bridge (mailer: mail). The functional suite and the 184 contract fixtures replay through Laravel’s HTTP kernel in CI; examples/laravel runs the shared walkthrough. Spec and decisions in docs/adapters/.
  • polaris/symfony (Polaris\Symfony\): a bundle whose polaris: configuration becomes the Polaris, Graph and Pipeline services on the application’s connection (a DSN, a PDO, a Doctrine DBAL connection), cache, logger, dispatcher and mailer; the polaris route loader mounts the 52 endpoints as named routes; the authenticator (custom_authenticators) guards the application’s firewalls with Polaris access tokens; polaris:schema:create, polaris:schema:drop and the CLI commands as polaris:*; a plain-text Symfony Mailer bridge (mailer: mail). The functional suite and the 184 contract fixtures replay through Symfony’s HTTP kernel in CI; examples/symfony runs the shared walkthrough.
  • polaris/yii (Polaris\Yii\): a yiisoft/config plugin whose polaris params become the Config, Polaris, Graph and Pipeline definitions on the application’s connection (a DSN, a PDO or a Yiisoft\Db connection), cache, logger, dispatcher and mailer; the 52 endpoints as routes in the routes group, PSR-15 straight through; an authentication method and the polaris/authentication middleware for the application’s own routes; the polaris:* commands for yiisoft/yii-console; a plain-text Yii mailer bridge (mailer: mail). The functional suite and the 184 contract fixtures replay through the Yii application in CI; examples/yii runs the shared walkthrough.
  • polaris/pdo: SchemaInstaller creates the tables and seeds the catalog on a connection, or drops them; the adapters’ migrations and commands call it.
  • polaris/cli: schema:create and schema:drop (--dsn, or a host’s connection).
  • polaris/core: Polaris\Notification\MailTemplates, the plain-text subjects and bodies the Symfony and Yii mail bridges send.
  • polaris/cli: schema:diff and doctor accept a host’s connection, secrets and auth settings; symfony/* constraints allow Symfony 8.

TypeScript client

  • @polaris-auth/client (packages/client-ts, npm): createClient({ baseUrl, token }) over openapi-fetch, typed by src/schema.d.ts, which openapi-typescript generates from polaris manifest --format=openapi; withToken() binds another token. Checked in and drift-checked in CI; the smoke test runs register, verify, login and /auth/me through the client against the Slim demo.
  • polaris manifest --format=openapi types every success response from the spec’s output.example (Polaris\Http\Manifest\ExampleSchema, JSON Schema by example; example_<variant> keys become a oneOf), so the generated client types data.

Polaris for PHP: the framework-agnostic extraction

The 1.0 Univeros module became a monorepo of framework-free packages with the same behaviour: polaris/core (Polaris\), polaris/psr15, polaris/pdo, polaris/testing, polaris/cli. Every 1.0 response is contract-frozen: the functional suite replays 184 request/response sequences (1,201 steps) recorded from the 1.0 code through the PSR-15 pipeline. The extraction is documented in docs/extraction/ (spec, decisions, the one behaviour change).

  • Wiring. Polaris::create(new Polaris\Wiring\Config(...)) builds the service graph without a container; Polaris\Psr15\Pipeline gives any PSR-15 host the ordered middleware and the request handler.
  • Persistence. Models are plain records; the schema is data (Polaris\Schema); repositories run on a DatabaseAdapter (polaris/pdo for PostgreSQL, MySQL and SQLite; an in-memory adapter in polaris/testing). The 18 Cycle migrations are replaced by polaris schema:export.
  • Routing. packages/core/api/**/*.yaml, shipped inside polaris/core, is the router: 52 endpoints, each with effect and receipt, loaded by Polaris\Http\Manifest; the CLI renders OpenAPI 3.1.
  • Ports. Repository, unit of work, tokens, identity provider, encrypter (SodiumEncrypter default, XChaCha20-Poly1305), rate store (PSR-16 default), metrics (PSR-3 default); PSR-14 listeners exposed through Polaris::listeners().
  • CLI. bin/polaris schema:export, schema:diff, manifest, doctor.
  • Demo. examples/slim: Slim 4 on SQLite with an executable walkthrough.
  • Behaviour change (documented). A request body field named like a request attribute no longer overrides the attribute (docs/extraction/behaviour-changes.md).

1.0.0 - 2026-06-11

First stable release: the authentication, MFA/OTP, and multi-tenant RBAC module for the univeros framework. A host registers one Module class and gets the full surface below as routes, entities, migrations, middleware, and PSR-14 events. See docs/auth/ for the specification and docs/auth/api-reference.md for the implemented HTTP contracts (52 endpoints).

Identity and sessions

  • Registration with email verification (single-use hashed tokens, enumeration-safe responses) and resend.
  • Password login with Argon2id hashing, transparent rehash, timing-equalized verification, sliding-window lockout, and optional verified-email and HIBP breach-check (k-anonymity) gates.
  • JWT access tokens (RSA, JWKS endpoint, key-rotation overlap window) plus opaque rotating refresh tokens with family-based reuse detection: replaying a rotated token revokes the whole session family.
  • Session management: device list, logout, logout-all, per-session revocation, and an optional instant access-token denylist.
  • Password forgot/reset/change with logout-everywhere semantics; reset and verification tokens are stored only as keyed HMACs.

MFA

  • TOTP (RFC 6238, encrypted secrets, QR provisioning), SMS, and email factors with enrollment + confirmation flows; ten single-use recovery codes issued on the first confirmed factor.
  • The login MFA gate: a short-lived single-purpose ticket bridges the password step to factor verification before any session is minted.
  • Step-up re-authentication for sensitive operations, stamped via auth_time; mfa/amr/auth_time persist across refresh and org switching.
  • OTP hygiene: codes stored as keyed HMACs, attempt budgets and consumption enforced by atomic conditional updates, send quotas and resend cooldowns against OTP bombing.

Multi-tenant RBAC

  • Organizations with soft delete, slug uniqueness, and per-org role templates (owner / admin / member) cloned from system templates; a global superadmin override for platform operators.
  • Members: listing (with PII gating of invited/suspended emails), role assignment, suspension with immediate org-scoped session revocation, and removal.
  • Single-use, expiring invitations bound to the invitee’s email.
  • Custom roles with a 12-key permission catalog; database-resolved authorization on every check (token claims are never trusted for authority).
  • Tenant invariants enforced beyond the permission check: no privilege escalation, owners protected from non-owners, last-owner protection, cross-tenant isolation on every org route.
  • User administration: read/update (self or admin), disable/enable, and anonymizing tombstone deletion.

Hardening and operations

  • Per-IP rate-limit budgets per endpoint group plus a global per-user budget across authenticated endpoints; user-agent sanitization at the edge.
  • Two security audits (#44 sign-off and the #97 follow-ups) fully remediated: atomic rotation/OTP/recovery claims, APP_KEY minimum length, full-length key fingerprints, typed challenge purposes, abuse caps.
  • Append-only audit log (actor, org, ip, user agent, whitelisted metadata), domain metrics counter, and notification listeners over a catalog of ~35 PSR-14 events.
  • 18 driver-portable migrations, scheduled pruning of expired transient rows, and a key-rotation runbook.
  • Verified by 519 tests (unit, persistence against a real driver, and end-to-end functional tests over the real middleware pipeline), with phpcs (PSR-12) and phpstan level 5 clean.

Documentation and agent experience

  • Full specification under docs/auth/ with the API reference and event catalog regenerated from the shipped code.
  • One YAML spec per implemented endpoint under api/, verified 1:1 against the route table.
  • An agent skill at .ai/skills/polaris/SKILL.md covering registration, configuration, the token model, the permission catalog, and the tenant invariants.