Migrate from 0.4 to 0.5
These changes tighten permission checks, open the settings root to every member and harden the account-security flows. Most projects only need the checklist; each section shows the code before and after.
Checklist
| Area | What to do | Section |
|---|---|---|
| Permissions | Store every ancestor id with a granted permission, in role fixtures and external resolvers | Permission ancestors |
| Settings | Nothing for the settings root; set memberAccess: true on a page that relied on inheriting the root's access | Settings root |
| Account security | Update clients and end-to-end tests that drive 2FA, password reset, email change or sign-out | Account security |
| Notification counts | Replace unseenCount / markAllSeen with unreadCount; drop calls to /unseen-count and /seen | Notification counts |
| New optional fields | Nothing to do | New fields |
A permission counts only with its ancestors
HasPermission now counts a permission a role holds only when every ancestor id is held too. An ancestor that is not a registered permission, or that every member holds (defaultGranted, such as settings or the memberAccess pages), counts as held. The wildcard *, module scopes and defaultGranted leaves work as before.
Roles saved through the DMS (the roles editor, /settings/workspace/roles/* and /api/tables/roles) are completed with their ancestor ids on save, so nothing changes for them. A role written another way, a test fixture, or an external resolver such as a plan definition, lists the whole chain:
// Before: the delete action alone let the action through
permissions: ["library.table-view.table-view-archive.table.delete"];
// After: the page, its table and their categories come with it
permissions: [
"library",
"library.table-view",
"library.table-view.table-view-archive",
"library.table-view.table-view-archive.table",
"library.table-view.table-view-archive.table.delete",
];
See Auth & permissions.
The settings root opens to every member
settingsCategory is memberAccess: "self": /settings opens to every signed-in member and lists only the settings they can open, and the roles editor no longer offers a "Settings" node (the workspace settings show at its top level). "self" opens the overview alone: a page declared under settingsCategory still needs its own permission.
memberAccess: true passes on to what is declared under it; "self" does not. A page that relied on inheriting the settings root's access declares memberAccess: true itself, or sits in a category of its own flagged true (Personal settings):
// Before: opened to every member through the page it is filed under
export class MyPrefsPage extends PageController("my-prefs", { displayName: "My preferences", category: AccountPage }) {}
// After
export class MyPrefsPage extends PageController("my-prefs", {
displayName: "My preferences",
category: AccountPage,
memberAccess: true,
}) {}
Account security
These flows changed for every user; update a client or an end-to-end test that drives them:
- Email 2FA setup:
POST /settings/user/security/two-factor/enable-emailstill takes{ currentPassword }but only sends a code to the account email.POST /settings/user/security/two-factor/confirm-email { code }turns the method on. - 2FA step:
POST /api/auth/verify-2faaccepts at most 5 codes per challenge, then answers 429error.too_many_2fa_attempts: sign in with the password again. An account takes at most 10 wrong codes within an hour across its challenges, then every code answers 429error.too_many_2fa_attempts_accountuntil the window passes, even after a new sign-in. A challenge opens one session, then answers 401error.2fa_challenge_used. The two-factor token carries a randomjti, so two sign-ins get two challenges. An authenticator code is accepted once. Every bad two-factor token (not a JWT, unknown user, bad signature, expired, wrong purpose) answers the same 401error.invalid_2fa_tokenonverify-2faandrequest-2fa-email, instead ofInvalid 2FA token,Invalid useror the token library's message (jwt expired,invalid signature), and a missing token answers 400error.invalid_2fa_token. - Current password: the forms that ask for it again (email change, password change, adding a 2FA method, account deletion) share 10 tries per account within 15 minutes, then answer 429
error.too_many_current_password_attempts, the right password included; the right password clears the count. - Stored 2FA email code:
User.twoFactorEmailCodeholds a SHA-256 hash of[userId, code], no longer the code. A module that writes it must store the hash. - Password reset: the code is matched in any case, is withdrawn after 5 wrong tries, and a reset signs out every session.
- Email change: the code is withdrawn after 5 wrong tries (429
error.email_change_too_many_attempts), and the one-code-a-minute limit survives a cancel and is checked before the current password: a second request within the minute answers 429error.rate_limitedeven with a wrong password. - Sign-out: an access token stops working as soon as its session is signed out or deleted, instead of at its expiry.
- Invitations:
POST /api/auth/validate-invite-token { token, email }tells a revoked, replaced, expired or used link apart. Signing up through an invitation always marks the email verified, with no validation round even underauth.mustValidateEmail. - Removed
skipEmailValidation: gone fromUserInvite,InviteUserToTenantOptionsandCreateUserInviteTokenOptions; an invite signup always validates the address. Stop passing it (TypeScript refuses it). The Members invite form and the invitation edit form no longer offer it.
The bell counts unread notifications
The header bell and the Notifications menu badge now show the same thing: the unread notifications, drawn in the tone of the most important one. The "unseen since the bell was last opened" count is gone:
useNotifications()no longer returnsunseenCount,markAllSeenorhandleRemoteSeen; readunreadCountand the newunreadTone.GET /settings/user/notifications/unseen-countandPUT /settings/user/notifications/seenare removed,/countsno longer returnsunseen(it returnsunreadTone), and thenotification:seenrealtime event is no longer published.- A nav badge may now carry a tone:
navBadge({ count })can return{ count, tone }, and a table tab takesnavBadge: { tone }. Several badges on one page add up in the strongest tone. See Navigation badges.
New optional fields
Additive, nothing to migrate:
User.twoFactorTotpLastStep: the last accepted authenticator time step.InviteResolution.inviteTokenandInviteResolutionsModel.getForToken(token).PageLayout.allComponentsHidden: set when a page declares blocks and the caller may see none.
Settings pages
Add your own pages to the settings area — workspace settings, personal settings every member reaches, their order, badges and frame.
Migrate from 0.7 to 0.8
The compatibility paths @antelopejs/interface-dms 0.8 and the DMS release that implements it remove, with the code before and after.