Repository navigation
v0.0.40
·
35 commits
to main
since this release
Immutable
release. Only release title and notes can be modified.
Fixed
- Behaviour change: evict expired site-lock attempt buckets, paginate personal/admin API token lists
(default and maximum API page size 100, withlimit/offset), preserve revoke-all
across pages, and recover concurrent duplicate share grants without a 500 (#640). - Share UTC timestamp helpers across sharing, tenants, background tasks, API tokens
and users (#640). - Users: publish deletion, disabling and new role assignments transactionally through
the durable outbox (inline cleanup without background tasks). Deletion purges tenant
memberships, shares to the user and direct permission grants; tenants left without
an owner are suspended. Existing roles no longer emitRoleAssigned(#627).
UserDeletednow lives insimple_module_core.user_events(still re-exported from
users.contracts.events) so modules can subscribe without depending onusers. - Frontend: verification failures show a generic error unless the server reports a bad
token or an already-verified account; file downloads bypass Inertia so signed-URL
redirects work; closed mobile sidebars are inert and hidden from assistive technology,
while desktop sidebars remain accessible (#638). - Settings: reverting a field preserves other unsaved edits; connection-test results
stay with their module, and unreadable or empty check responses report failure (#637).
@simple-module-py/ui:readBodynow rethrows a body-read failure instead of returning
null;apiFetchreports it as a network failure (status 0). - Release tooling converts Python prereleases to npm SemVer and publishes them
with thenexttag; metadata checks catch missing/stale npm matrix entries.
Memray loadtests refuse occupied ports and stop only their own process group (#629). - Release workflow serializes runs, rejects non-main or stale dispatches before
publishing, tags the published release commit and merges concurrent main changes
into it before atomically pushing main and the tag, failing safely on conflicts (#628). - CLI:
smpy updateselects a tag accepted by every sibling's constraint;
git sources preserve refs containing/, and Python prerelease tags sort before
their final release (#636). - Hosting: derive the production Inertia asset version from the Vite manifest hash so
stale tabs reload after deployment; include CSS from statically imported chunks (#633). - Hosting: unwind started modules and dispose the database engine after lifecycle failures;
serialize setup-wizard migrations across workers, including fresh databases (#634). - Database: discard
on_commitcallbacks registered inside a rolled-back savepoint,
including released child savepoints, while preserving callbacks from surviving work (#631). - Role-permission edits, module settings updates/clears and system/tenant feature-flag
overrides now apply only after commit and reload across workers through the
invalidation bus. Periodic reloads bound staleness when messages are missed (#621). - Keycloak user-cache inserts and profile updates now commit in an independent transaction,
preserving user IDs across logins and bearer requests. Concurrent first logins reuse the
winning cache row; bearer requests throttle last-login writes to once per five minutes (#618).
Added
- Weekly Dependabot updates for uv, npm and GitHub Actions (#629).
tenants.contracts.UsageMeter: an implementation-free usage-metering protocol next to
EntitlementProvider(check,consume,record,refund,reserve/commit/release,
summary) withUsageOutcome/UsageReservation/UsageSummaryRow,UsageExceededError,
anUnlimitedUsageMeterdefault onapp.state.tenants.usageandget_usage_meter(app)(#616).simple_module_hosting.csrf.rotate_csrf_token(request)for principal changes that keep the
session (#615).
Security
- Remove account-status and superuser fields from the users
UserUpdatecontract;
self-profile routes already used a narrower input schema (#640). - Inertia:
Inertia.back()and the exported validation handler now redirect only to a
same-site path taken fromReferer, falling back to/; an off-site or malformed
Refererpreviously produced an open redirect (#635). - Users: account administration never exceeds the actor's own authority. Outside a full
admin session, a role can be granted and an account managed (edit, disable, reset link,
invite) only when its effective permissions (roles plus direct grants) are within the
actor's: a delegate holdingusers.manage, or an admin's scoped token, cannot grant
admin, cannot reach admin/superuser accounts, and cannot grant a role mapped to
permissions it lacks. Nobody can disable or demote themselves or the last active
administrator (#624). - Users: OAuth sign-in links to an existing account by email, or marks a new account
verified, only with the provider's proof that the email is verified: OIDC
email_verified: true, Google's People APImetadata.verifiedon the primary email,
a verified GitHub address from/user/emails; Microsoft (Entra) email claims are never
proof. An unverified account (SSO-provisioned ones included, pending invites excepted)
never absorbs a later OAuth login, closing an account pre-hijacking path (#622).
Behaviour change: a provider without proof cannot sign in to an existing account by
matching its email alone (400 OAUTH_USER_ALREADY_EXISTS); existing subject links still
sign in. - Tenants: accepting an invitation requires a verified email (#623). Verification is read
fresh from the user directory (newDirectoryUser.email_verified;Nonewhen the
provider doesn't track it, in which case Keycloak'semail_verifiedtoken claim decides),
so an account verified after signing in can accept without signing in again.
Behaviour change: withusers.require_verificationoff, self-registered accounts must
verify their email (or be marked verified by an admin) before accepting an invitation. - Users: store access and refresh bearer tokens as SHA-256 digests and prune spent
credentials in bounded batches on login/refresh, retaining active-family
reuse evidence (#626). Behaviour change: migrationa626b1c8d023deletes
existing access/refresh tokens; affected clients must log in once again. Logging out
(POST /users/logout,POST /api/users/auth/logout) now deletes the presented access
token server-side instead of only clearing the cookie. - Users: atomically consume refresh tokens and revoke their refresh-token family on reuse
(#625). Migrationa625f0b7c912adds family tracking; existing tokens start separate families. - DB: sessions bound to a SQLAlchemy
Connectionnow inherit its engine's tenant
policy and audit configuration, preserving strict tenant isolation and audit callbacks (#630). - Keycloak JWKS fetches now use a per-cache lock and a 30-second minimum interval
between attempts, including failures, preventing random-kidbearer tokens from
triggering an outbound request each. Newly rotated keys may take up to 30 seconds
to be accepted (#620). - Keycloak sign-in clears prior identity state while preserving registered browser gates and
the safe post-login redirect; validates the ID token, nonce and matching subject (#619).
Behaviour change: browser sessions renew server-side via refresh token at access-token
expiry, using validated claims for fresh roles. Demoted or disabled users lose access
at the next access-token expiry; failed refreshes or expired sessions without a refresh
token require sign-in. The ID token is no longer stored in the signed cookie to keep
it below browser size limits; logout usesclient_idinstead ofid_token_hint.
Sessions created before this release carry no refresh token, so every Keycloak browser
session signs in once after upgrading. Realms with "Revoke Refresh Token" enabled should
allow a refresh-token reuse count of at least 1, or parallel requests at expiry can sign a
user out. - Sign-in now rotates the session: users'
on_after_login(password, OAuth, accept-invite)
clears it and keeps only the pending post-login redirect, so a CSRF token (or any other
session state) issued before sign-in no longer survives it. Keycloak sign-in rotates the CSRF
token (#615). Behaviour change: session keys a module stored
before sign-in are dropped at sign-in.
simple_module_hosting.session.preserve_across_sign_in(app, key)lets a module keep browser-bound
gate state across it; site_lock uses it so the unlock marker survives sign-in.