Repository navigation
v0.7.7
Upgrading
- Run
kit:update plan|applyforcore(re-pinnedsha256:e238b630…7408→sha256:dc02fc76…f634) andshell(sha256:93780271…035d→sha256:c7036fbf…8b21). The breadcrumb trail moves from the shell's row into<Page>, above the heading (see below). Three things for an app:- Pages without
<Page>lose their trail, because the shell no longer draws one. Render<Breadcrumbs />(from@kit/ui) above the page's own heading. - An edited
Partials/Breadcrumbs.vueconflicts on update. The shell's copy is now a thin wrapper around core'sBreadcrumbs, kept for pages that import it; the trail itself isKit/Layout/Breadcrumbs.vue, which you edit or replace through@kit/ui. - Headings move up. The 40px row under the header is gone, and a 20px trail line plus an 8px gap sits above the heading. So every page's H1 is about 30px higher than before, and at the same height on every page.
- Pages without
- Run
kit:update plan|applyforsession-ui(re-pinnedsha256:c5b8919e…e94e→sha256:d96afd41…1b7f) andworkspace-ui(re-pinnedsha256:5d746402…9202→sha256:749a17ac…5112), thenphp artisan migrate: the new0001_01_02_000505_add_finished_at_to_kit_account_deletions_tablemigration, andWorkspace/Settings/Delete/Page.vueandWorkspace/Account/Delete/Page.vue, which show the password field whileconfirms_passwordis true. A host that edited those pages adds the field itself. Clients that callDELETE /@{workspace}/settings/deletemust now sendpassword, unlesskit.lifecycle.confirm_passwordisfalse. - Run
kit:update plan|applyforaccess(re-pinnedsha256:a179066f…6d98→sha256:98af2e26…2de6) andsettings(re-pinnedsha256:fb83eb1d…5f1e→sha256:37ad7378…6eba): their000300and000400migrations now adopt a compatible host table in place. A host that has already run them is unaffected (migrate doesn't re-run them). A host with its ownentitlements,complimentary_grantsor spatie settings table should runkit:adopt --json(orkit:add's plan) first: an incompatible table now blocks with its columns named instead of failing inmigrate. - Run
kit:update plan|applyforcore(re-pinnedsha256:6e87fc26…fe56→sha256:e67b1978…efa5):kit.csstakes the change below. A host that defined its own--container-2kcan drop it; its own value still wins if kept. - Run
kit:update plan|applyforcore(re-pinnedsha256:b7a9b9ba…5e9d→sha256:6e87fc26…fe56):Kit/Form/Slider.vuetakes the fix below. - No feature is re-pinned: the mail views and account mail ship in the package. To show the brand's lockup in mail, add a PNG of it for a light background (2x the 220px it shows at, or more) and set
KIT_BRAND_MAIL_LOCKUP(kit.brand.mail_lockup) to its path inpublic/or an absolute file path. Without it, the mail header is the product's name, centred. - A host that published its own
resources/views/vendor/mailkeeps its views. To leave the manage-notifications link off security mail there too, give itsmessageandfooterviews themanageprop (see docs/mail.md › Account mail). - A host that set its own
ResetPassword::toMailUsing()orVerifyEmail::toMailUsing()keeps it, and doesn't get the kit's account mail. - Run
kit:update plan|applyforworkspaces(re-pinnedsha256:40fdd76c…81c4→sha256:56912cc3…eafd) andsession-ui(re-pinnedsha256:abbeb463…c64f→sha256:a4581c16…377d), thenphp artisan migrate: the new0001_01_02_000210_add_deleting_at_to_kit_workspaces_table(nullabledeleting_atanddeleting_byonworkspaces, recorded as kit-added on an adopted table) and0001_01_02_000504_create_kit_account_deletions_tablemigrations below. Existing workspaces stay open. Nothing changes for a host whose purge handler doesn't opt into deferral. - Run
kit:update plan|applyforsession-ui(re-pinnedsha256:a4581c16…377d→sha256:c5b8919e…f94e):0001_01_02_000501_add_user_foreign_key_to_kit_security_events_table.phpand0001_01_02_000503_add_user_foreign_key_to_kit_user_state_table.phptake the type fix below. It's a type-only change, so there's nothing to migrate: a host that already ran them keeps its schema, andkit:update plan's note that a changed migration won't reach the database needs no action here. - Run
kit:update plan|applyforworkspace-ui(re-pinnedsha256:2f226d38…39e7→sha256:5d746402…9202):Workspace/Placeholder/Page.vue,Workspace/Settings/Placeholder/Page.vueandWorkspace/Settings/People/Page.vuetake the type fix below. No behaviour changes. - The capability fix below is in the package and re-pins nothing. A host that adopted
settingsonly so People would open, or so owners could manage others' MCP connections, no longer needs it for that. Keep it if you use its General section or its MCP settings tools, and for the AI assistants page whilekit.mcp.connection_abilityis the defaultsettings.read(kit:statusflags this). - The kit now refuses to boot when an ability is gated on a capability no adopted feature declares, naming the ability, the capability and the feature. A host whose own provider calls
Registry::abilities()keeps booting: the check covers what the kit's features contribute. kit:statusnow flags declared permissions missing from the database as a blocking step.php artisan migrate --forceputs them back.- Run
kit:update plan|applyforcore(re-pinnedsha256:e67b1978…efa5→sha256:145cf85d…412e),mcp(re-pinnedsha256:e35e2879…7ae0→sha256:760d2e0a…d924),workspace-ui(re-pinnedsha256:5d746402…9202→sha256:0e1a0d0e…3320) andnotes(re-pinnedsha256:1a0455e7…551b→sha256:cc6e2adb…bb95):DataTable.vue,ActivityEntryRow.vue, the Connections, People and Security pages andNoteCard.vuetake the table and date fixes below. A host that copiestypes/kit.d.tsre-copies it:TableColumnhasdate. Nothing to migrate.
Tests
- The resources check (
composer test:resources) no longer copiesvendor,node_modules,browserortypecheckinto each disposable package copy. It reads onlysrcandresources. All 15 tamper cases still run, and the check goes from about 50s to about 6s locally. CI gives each test step its owntimeout-minutes(4 for the composer checks, 6 for the browser run), so a hang fails its own step quickly instead of holding the job until its 10 to 15 minute limit. - Run
kit:update plan|applyforsession-ui(re-pinnedsha256:c5b8919e…f94e→sha256:fae5ea29…bcb6): this addsAuth/Register/Page.vue; hosts with their own register page can drop it.Auth/Login/Page.vuetakes the "Create an account" link below. A host that copiestypes/kit.d.tsre-copies it:LoginViewModelhasregister, andRegisterViewModelis new. Registration stays closed by default, so nothing changes until an app opens it.
Added
-
Deleting a workspace asks for your password (docs/lifecycle-events.md › Delete a workspace). Settings › Delete workspace now requires the current
passwordwith the typed name. A missing or wrong one is a 422 and nothing changes. Account deletion already asked for it. Both are on by default.kit.lifecycle.confirm_password(KIT_DELETE_CONFIRM_PASSWORD) turns them off, but only when it isfalseexactly: any other value keeps the password required. -
A host can keep the user row when an account is deleted (docs/lifecycle-events.md › Keeping the user row). Bind
Tey\Kit\Features\SessionUi\Contracts\DeletesAccountRow::delete(Model $user)to anonymise the row instead of deleting it, for tables that keep history on RESTRICT foreign keys to users. Unbound, the user is hard-deleted as before. After the kit's rows go, an anonymised row keeps its Concierge account deactivated, so it never signs in. Itskit_account_deletionsrecord is markedfinished_at, andFinishAccountDeletionreturnsfalseon a retry instead of failing. -
Adopt-in-place for the access and settings tables. A host's own
entitlementsandcomplimentary_grantstables, and spatie/laravel-settings' table, are adopted when compatible (the columns the kit reads and writes in a compatible type, nullable where the kit writes null, an auto-incrementingid, the kit's unique index, and no other required host column on a table the kit inserts into): nothing is created or changed on them, and each is recorded inkit_adoptions. Settings then creates onlysettings_intents. Plans list each underadoptions, andkit:adopt --jsonwith no feature lists each such table undertablesasadopted,adoptableorconflict. See Adopting existing access and settings tables.composer test:table-adoptcovers it on SQLite and PostgreSQL. -
max-w-2k(160rem), the wide page measure the apps share, as a--container-2ktoken inkit.cssunder@theme default, so a host's own value wins. -
/mcp storm guards (
kit.mcp.limits,kit.mcp.list_cache_seconds). An assistant that reconnects in a burst no longer fills every PHP worker. Each access token may have 4 requests in flight and 120 a minute; over either, the request gets a 429 withRetry-After(a JSON-RPC error body) before it does any work. An app-wide in-flight cap (total) is off by default: set it below your PHP worker count. Slots live in the cache store and free when the request ends, or afterhold_secondsif the worker died.tools/listanswers are reused per connection for 60 seconds; a call still checks its tool's gate, so a list stale by a permission change only lists. A host that publishedconfig/kit.phpgets these defaults without adding the keys.composer test:mcp-limitscovers both. -
Deferred workspace and account deletion (docs/lifecycle-events.md › Deferred deletion). A purge handler that also implements
Tey\Kit\Features\Workspaces\Contracts\DefersWorkspacePurge(orTey\Kit\Features\SessionUi\Contracts\DefersAccountPurge) is no longer run inside the request's transaction. The kit closes the workspace at once (deleting_at), dispatchesWorkspaceDeleting, writes aworkspace.deletion_startedaudit row, and after the commit calls the handler'sbegin()to queue the host's job. That job purges in its own chunks and ends withapp(FinishWorkspaceDeletion::class)($workspaceId), which deletes the kit's and Concierge's rows in one short transaction and dispatchesWorkspaceDeleted. The hook is idempotent (a second call returns false) and refuses a workspace that isn't being deleted (workspace_not_deleting). A closed workspace refuses every member's admission (workspace_deleting, 403), takes no invitation or people change, is hidden from the switcher and lists, and sends its members from its pages to the workspace list with a "being deleted" notice. It's never reopened. Accounts work the same way: the account is closed at once (workspaces left, Concierge account deactivated so sign-in is refused, database sessions ended, Passport tokens revoked and Sanctum tokens deleted), thenapp(FinishAccountDeletion::class)($userId)deletes it. The synchronous mode stays the default and is unchanged.composer test:lifecycle-pagescovers both modes. -
A Register page (
Auth/Register, session-ui; README › session-ui). While an app turns on Fortify'sregistrationfeature and Concierge's registration mode isn't closed,/registerrenders it (Fortify'sregisterView) and posts to Fortify, so the account is still the app'sCreatesNewUsersaction. It matches the other guest cards:novalidatewith field errors in the card, name focused on load, FormStatus, both passwords cleared after every attempt, and a 429, an expired session (419) or a network failure shown in the card instead of Inertia's error modal. Sign in shows a "Create an account" link in Concierge's open mode, and Register links back to sign in. An app's own sign-up (an invited account, say) renders the same page from its own route with aRegisterViewModelof its own, so its copy stays the kit's:action,heading,leadandsubmitLabel, an address shown as text instead of the email input (email,emailHint), values posted that aren't inputs (submit), and its own 429 copy (throttleMessage). An error on a key with no input (invitation, oremailshown as text) is one alert at the top of the card.composer test:registercovers the page, its props, the gating, a registration and its validation errors; the browser job covers the card on the real@inertiajs/vue3;bin/typecheck-components.shtype-checks it on Inertia 3.7.1 and current. -
Run
kit:update plan|applyforcore(re-pinnedsha256:b7a9b9ba…5e9d→sha256:a8184211…bf3e):Kit/Form/submitForm.ts,Kit/Form/formFrame.ts,Kit/Form/FormActions.vue,Kit/Workbench/Workbench.vueandKit/Workbench/workbench.tstake the Save gate and the status fix below, and the kit'suilang file gainsform.submit_blocked.*. A host that disabled Save by hand during an upload, or watched its form to clear "Not saved", can drop that.
Added
- Save is disabled while it can't succeed (docs/submit-form.md › When Save is disabled).
FormActionsand a Workbench with:formdisable Save, and ignore Enter and ⌘S, while pending work is in flight (an upload), a 409 stale-write conflict is open, or the host'scanSubmitis false.canSubmitis a new prop on both (can-submit) and auseSubmitFormoption, a boolean,Refor getter,trueby default; while it's falsesubmit()resolvesfalsewithout calling the handler. The button carriesdata-blockedand anaria-describedbywith the reason, fromkit::uiform.submit_blocked.*("Waiting for uploads to finish").submitBlock()andsubmitBlockMessages()are exported.tests/js/submit-form.test.mjsandworkbench-form.test.mjscover each reason; the browser job checks a pending upload,can-submitfalse and an open 409 on the Workbench.
Changed
- The breadcrumb trail is a small line above the page's heading, not a row in the shell (as GOV.UK and Polaris place it).
<Page>draws it in the shell: the page's ancestors, with the current page left out because its heading names it. It's drawn whether or not the page has a heading.- Home page. With no ancestors, the line keeps its 20px as an
aria-hiddenspacer, never an empty landmark, so the heading sits at the same height on every page. Measured in a headless render: the H1 is at 113px at 1280 wide and 109px at 390, on both the dashboard and Settings › People. - Pages without
<Page>.Breadcrumbsis exported from@kit/ui(with acrumbsprop) for pages that don't use<Page>.
- Home page. With no ancestors, the line keeps its 20px as an
- The web footer's docs now match the brand standard (2026-10-29): a © line is optional and never flagged. The accepted form is
© {year} {Product} · Built by Tey Labs Inc.; kit's default footer still adds none. No behaviour change. - The mail header is the brand lockup, centred (docs/mail.md › The header: the brand lockup). The branded layout is now Laravel's base markdown layout with the lockup image centred above the message, linked to
app.url: 220px wide, its height kept to the image's ratio, and the product's name as alt text. It's a local PNG embedded inline as a CID attachment, never a remote image. With no lockup, a missing file or a non-PNG, the header is the product's name, centred, and never a broken image. The left-aligned mark, bold name and accent rule are gone. Buttons and links keep the brand accent.kit:statusreports a configured lockup that isn't a PNG as attention, not blocking. - Password reset and email check mail are the kit's (docs/mail.md › Account mail). With session-ui, Fortify's reset and verification mail use the kit's copy instead of Laravel's stock "Hello!", with sentence-case buttons ("Reset password", "Verify email address"). As security mail, they have no "Manage notifications" link.
FluentMailMessage::security()does the same for a host's own security mail.
Fixed
- An existing access or settings table is never created over. A plan meeting an incompatible one blocks with a conflict naming the missing, mistyped or non-nullable columns, the host's required columns and the missing unique index, and the migration refuses it before creating anything. A table whose schema can't be read is a refusal, not a create. Before, an existing
complimentary_grantsorentitlementswas a bare collision, and a host's spatie settings table wasn't detected at all, somigratefailed on it. - DataTable sits on the card surface. The table and its two empty states had no background, so rows showed the page ground. They now use
bg-card text-card-foreground, the sticky first column matches, and column headers no longer wrap ("Connected by"). - Dates show through DateTime, not as raw ISO values. A
TableColumn(andTableQuery::column()) takesdate, a DateTime format, and DataTable renders that column throughDateTime: the viewer's zone, the full value intitle. AI assistants' Connected (a raw2026-…T…+00:00) and Expires, People's joined, sent and expiry dates, Security's sessions and log, Notes and activity entries now all use it. - Removing one MCP grant is no longer styled destructive. On AI assistants only Revoke stays destructive; "Remove " is a plain row action.
- Avatar upload and removal work on PostgreSQL with string or uuid morphs. Avatars compared
media.model_id(varchar there) with an integer, a 500 ("operator does not exist: character varying = integer"). The lookups and the avatar holder's media relation now bind the id as a string. - People opens, and owners manage MCP connections, without the
settingsfeature. workspace-ui gatesmembers.manageand mcp gates managing connections onworkspace.settings, but onlysettingsdeclared that capability. A host withoutsettingsnever defined it, so it read as off: People was a 404 for every owner, and so was managing another member's MCP connection, whateverkit.capabilitiessaid. workspace-ui and mcp now declareworkspace.settingsthemselves. A capability declared by more than one feature is kept once. - Workspace invites offer member roles only. People, its Invite modal and Change role never offer a scoped Concierge role (such as
guest), even when a host'sGrantPolicylists one, and an invite or role change posted with a scoped role is refused (403,role_not_grantable). - A missing declared permission comes back on the next migrate. The deploy sync skipped whenever the catalog fingerprint was unchanged, even if the database had lost a declared permission. It now syncs again while any declared permission is missing.
Slidertype-checks under vue-tsc.:aria-valuetexttested thevalueTextfunction prop itself, which TypeScript reports as always true (TS2774) and fails a host'svue-tsc; it now tests whether the prop was given.- A failed save no longer stays "Not saved" after further edits (docs/workbench.md › Form frame). A Workbench with
:formwhose form was already dirty kept "Not saved" through every later edit. The next edit now puts the bar back to "Unsaved changes" (editedSaveState), for auseSubmitFormand auseFormalike. The browser job checks both. - Security: asking for a reset link no longer tells whether an account exists (docs/onboarding.md › Password reset). Fortify answered an unknown address (or a throttled one) with an
emailerror, a 422 for JSON, so the form could test addresses. session-ui now answers both of Fortify's reset-link responses the same: back with thestatusflashkit::ui.auth.reset_link_sent("If an account exists for that email, we've sent a password reset link."), or200 {"message"}for JSON. A malformed address is still a 422 and throttling a 429. Only Fortify's defaults are replaced, so a host's own binding still wins; a host with its own neutral binding can drop it.composer test:onboardingchecks an existing account, an unknown one and a well-formed address nobody has get byte-identical redirects, flashes and JSON. - Mail links are https on an https app. The manage-notifications link and the password reset link were built from the current request, so behind a TLS-terminating proxy they could read
http://. They're now built onapp.url. - session-ui's user foreign key migrations pass a host's PHPStan.
000501and000503typeduser()as a barearray(missingType.iterableValue) and built the user model withnew (config(...)), anobjectwithoutgetTable()orgetKeyName()(method.notFound): six errors at level 7 in a host that analysesdatabase/.user()now returnsarray{string, string}, and the configured class is read asclass-string<Model>. The model is built and read exactly as before. A newcomposer test:phpstan-stubs(bin/phpstan-stubs.sh, a CI step) analyses every PHP stub, with.stubdropped, at PHPStan level 8 against the kit's vendor. - workspace-ui's pages type-check on vue-tsc 2. The Placeholder pages and Settings › People cast to an inline object type in the template (
as { label: string; url: string }[]), which vue-tsc 2 can't parse (TS1005). Each page now names the type in<script setup>and casts to that name. The components type-check (bin/typecheck-components.sh, CI's vue-tsc job) now covers every Vue and TypeScript file a feature installs intoresources/js(eachfeature.jsonfilesentry, at its installed path), not only core's components, the shell and the guest pages, and runs on vue-tsc 2.2.12 (installed asvue-tsc-2) as well as 3, each against Inertia 3.9.1 and 3.7.1.
What's Changed
- Answer reset-link requests neutrally by @jaspertey in #494
- Center the brand lockup in mail, embedded inline; kit account mail by @jaspertey in #498
- Add deferred workspace and account deletion by @jaspertey in #506
- Slider: fix vue-tsc TS2774 on aria-valuetext by @jaspertey in #509
- Type session-ui's user FK migrations and workspace-ui's template casts for host checkers by @jaspertey in #499
- Declare every capability an ability is gated on, fail closed at boot, resync lost permissions, member-only invite roles by @jaspertey in #512
- Guard /mcp against reconnect storms by @jaspertey in #522
- Faster resources check; per-step CI timeouts by @jaspertey in #526
- Ship the shared max-w-2k (160rem) container token by @jaspertey in #311
- Describe the footer's © line as optional, per the brand standard by @jaspertey in #310
- Let a host keep the user row on deletion, and confirm the password to delete by @jaspertey in #514
- Sweep: #294, #517, #507, #505 in one merge by @jaspertey in #528
- Adopt compatible access and settings tables in place by @jaspertey in #294
- Disable form Save while it can't succeed; clear Not saved on the next edit by @jaspertey in #505
- Add a session-ui Register page by @jaspertey in #507
- DataTable card surface, date columns, nowrap headers; plain grant removal; string avatar model_id by @jaspertey in #517
- Breadcrumbs move into Page above the heading; the shell row goes (2702) by @jaspertey in #332
- Release v0.7.7 by @jaspertey in #530
Full Changelog: v0.7.6...v0.7.7