Skip to content

v0.7.7

Choose a tag to compare

@jaspertey jaspertey released this 02 Nov 13:27
d4d82d1

Upgrading

  • Run kit:update plan|apply for core (re-pinned sha256:e238b630…7408 → sha256:dc02fc76…f634) and shell (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.vue conflicts on update. The shell's copy is now a thin wrapper around core's Breadcrumbs, kept for pages that import it; the trail itself is Kit/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.
  • Run kit:update plan|apply for session-ui (re-pinned sha256:c5b8919e…e94e → sha256:d96afd41…1b7f) and workspace-ui (re-pinned sha256:5d746402…9202 → sha256:749a17ac…5112), then php artisan migrate: the new 0001_01_02_000505_add_finished_at_to_kit_account_deletions_table migration, and Workspace/Settings/Delete/Page.vue and Workspace/Account/Delete/Page.vue, which show the password field while confirms_password is true. A host that edited those pages adds the field itself. Clients that call DELETE /@{workspace}/settings/delete must now send password, unless kit.lifecycle.confirm_password is false.
  • Run kit:update plan|apply for access (re-pinned sha256:a179066f…6d98 → sha256:98af2e26…2de6) and settings (re-pinned sha256:fb83eb1d…5f1e → sha256:37ad7378…6eba): their 000300 and 000400 migrations 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 own entitlements, complimentary_grants or spatie settings table should run kit:adopt --json (or kit:add's plan) first: an incompatible table now blocks with its columns named instead of failing in migrate.
  • Run kit:update plan|apply for core (re-pinned sha256:6e87fc26…fe56 → sha256:e67b1978…efa5): kit.css takes the change below. A host that defined its own --container-2k can drop it; its own value still wins if kept.
  • Run kit:update plan|apply for core (re-pinned sha256:b7a9b9ba…5e9d → sha256:6e87fc26…fe56): Kit/Form/Slider.vue takes 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 in public/ 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/mail keeps its views. To leave the manage-notifications link off security mail there too, give its message and footer views the manage prop (see docs/mail.md › Account mail).
  • A host that set its own ResetPassword::toMailUsing() or VerifyEmail::toMailUsing() keeps it, and doesn't get the kit's account mail.
  • Run kit:update plan|apply for workspaces (re-pinned sha256:40fdd76c…81c4 → sha256:56912cc3…eafd) and session-ui (re-pinned sha256:abbeb463…c64f → sha256:a4581c16…377d), then php artisan migrate: the new 0001_01_02_000210_add_deleting_at_to_kit_workspaces_table (nullable deleting_at and deleting_by on workspaces, recorded as kit-added on an adopted table) and 0001_01_02_000504_create_kit_account_deletions_table migrations below. Existing workspaces stay open. Nothing changes for a host whose purge handler doesn't opt into deferral.
  • Run kit:update plan|apply for session-ui (re-pinned sha256:a4581c16…377d → sha256:c5b8919e…f94e): 0001_01_02_000501_add_user_foreign_key_to_kit_security_events_table.php and 0001_01_02_000503_add_user_foreign_key_to_kit_user_state_table.php take 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, and kit:update plan's note that a changed migration won't reach the database needs no action here.
  • Run kit:update plan|apply for workspace-ui (re-pinned sha256:2f226d38…39e7 → sha256:5d746402…9202): Workspace/Placeholder/Page.vue, Workspace/Settings/Placeholder/Page.vue and Workspace/Settings/People/Page.vue take the type fix below. No behaviour changes.
  • The capability fix below is in the package and re-pins nothing. A host that adopted settings only 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 while kit.mcp.connection_ability is the default settings.read (kit:status flags 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:status now flags declared permissions missing from the database as a blocking step. php artisan migrate --force puts them back.
  • Run kit:update plan|apply for core (re-pinned sha256:e67b1978…efa5 → sha256:145cf85d…412e), mcp (re-pinned sha256:e35e2879…7ae0 → sha256:760d2e0a…d924), workspace-ui (re-pinned sha256:5d746402…9202 → sha256:0e1a0d0e…3320) and notes (re-pinned sha256:1a0455e7…551b → sha256:cc6e2adb…bb95): DataTable.vue, ActivityEntryRow.vue, the Connections, People and Security pages and NoteCard.vue take the table and date fixes below. A host that copies types/kit.d.ts re-copies it: TableColumn has date. Nothing to migrate.

Tests

  • The resources check (composer test:resources) no longer copies vendor, node_modules, browser or typecheck into each disposable package copy. It reads only src and resources. All 15 tamper cases still run, and the check goes from about 50s to about 6s locally. CI gives each test step its own timeout-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|apply for session-ui (re-pinned sha256:c5b8919e…f94e → sha256:fae5ea29…bcb6): this adds Auth/Register/Page.vue; hosts with their own register page can drop it. Auth/Login/Page.vue takes the "Create an account" link below. A host that copies types/kit.d.ts re-copies it: LoginViewModel has register, and RegisterViewModel is 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 password with 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 is false exactly: 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. Its kit_account_deletions record is marked finished_at, and FinishAccountDeletion returns false on a retry instead of failing.

  • Adopt-in-place for the access and settings tables. A host's own entitlements and complimentary_grants tables, 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-incrementing id, 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 in kit_adoptions. Settings then creates only settings_intents. Plans list each under adoptions, and kit:adopt --json with no feature lists each such table under tables as adopted, adoptable or conflict. See Adopting existing access and settings tables. composer test:table-adopt covers it on SQLite and PostgreSQL.

  • max-w-2k (160rem), the wide page measure the apps share, as a --container-2k token in kit.css under @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 with Retry-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 after hold_seconds if the worker died. tools/list answers 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 published config/kit.php gets these defaults without adding the keys. composer test:mcp-limits covers both.

  • Deferred workspace and account deletion (docs/lifecycle-events.md › Deferred deletion). A purge handler that also implements Tey\Kit\Features\Workspaces\Contracts\DefersWorkspacePurge (or Tey\Kit\Features\SessionUi\Contracts\DefersAccountPurge) is no longer run inside the request's transaction. The kit closes the workspace at once (deleting_at), dispatches WorkspaceDeleting, writes a workspace.deletion_started audit row, and after the commit calls the handler's begin() to queue the host's job. That job purges in its own chunks and ends with app(FinishWorkspaceDeletion::class)($workspaceId), which deletes the kit's and Concierge's rows in one short transaction and dispatches WorkspaceDeleted. 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), then app(FinishAccountDeletion::class)($userId) deletes it. The synchronous mode stays the default and is unchanged. composer test:lifecycle-pages covers both modes.

  • A Register page (Auth/Register, session-ui; README › session-ui). While an app turns on Fortify's registration feature and Concierge's registration mode isn't closed, /register renders it (Fortify's registerView) and posts to Fortify, so the account is still the app's CreatesNewUsers action. It matches the other guest cards: novalidate with 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 a RegisterViewModel of its own, so its copy stays the kit's: action, heading, lead and submitLabel, 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, or email shown as text) is one alert at the top of the card. composer test:register covers 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.sh type-checks it on Inertia 3.7.1 and current.

  • Run kit:update plan|apply for core (re-pinned sha256:b7a9b9ba…5e9d → sha256:a8184211…bf3e): Kit/Form/submitForm.ts, Kit/Form/formFrame.ts, Kit/Form/FormActions.vue, Kit/Workbench/Workbench.vue and Kit/Workbench/workbench.ts take the Save gate and the status fix below, and the kit's ui lang file gains form.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). FormActions and a Workbench with :form disable Save, and ignore Enter and ⌘S, while pending work is in flight (an upload), a 409 stale-write conflict is open, or the host's canSubmit is false. canSubmit is a new prop on both (can-submit) and a useSubmitForm option, a boolean, Ref or getter, true by default; while it's false submit() resolves false without calling the handler. The button carries data-blocked and an aria-describedby with the reason, from kit::ui form.submit_blocked.* ("Waiting for uploads to finish"). submitBlock() and submitBlockMessages() are exported. tests/js/submit-form.test.mjs and workbench-form.test.mjs cover each reason; the browser job checks a pending upload, can-submit false 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-hidden spacer, 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>. Breadcrumbs is exported from @kit/ui (with a crumbs prop) for pages that don't use <Page>.
  • 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:status reports 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_grants or entitlements was a bare collision, and a host's spatie settings table wasn't detected at all, so migrate failed 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 (and TableQuery::column()) takes date, a DateTime format, and DataTable renders that column through DateTime: the viewer's zone, the full value in title. AI assistants' Connected (a raw 2026-…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 settings feature. workspace-ui gates members.manage and mcp gates managing connections on workspace.settings, but only settings declared that capability. A host without settings never defined it, so it read as off: People was a 404 for every owner, and so was managing another member's MCP connection, whatever kit.capabilities said. workspace-ui and mcp now declare workspace.settings themselves. 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's GrantPolicy lists 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.
  • Slider type-checks under vue-tsc. :aria-valuetext tested the valueText function prop itself, which TypeScript reports as always true (TS2774) and fails a host's vue-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 :form whose form was already dirty kept "Not saved" through every later edit. The next edit now puts the bar back to "Unsaved changes" (editedSaveState), for a useSubmitForm and a useForm alike. 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 email error, 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 the status flash kit::ui.auth.reset_link_sent ("If an account exists for that email, we've sent a password reset link."), or 200 {"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:onboarding checks 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 on app.url.
  • session-ui's user foreign key migrations pass a host's PHPStan. 000501 and 000503 typed user() as a bare array (missingType.iterableValue) and built the user model with new (config(...)), an object without getTable() or getKeyName() (method.notFound): six errors at level 7 in a host that analyses database/. user() now returns array{string, string}, and the configured class is read as class-string<Model>. The model is built and read exactly as before. A new composer test:phpstan-stubs (bin/phpstan-stubs.sh, a CI step) analyses every PHP stub, with .stub dropped, 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 into resources/js (each feature.json files entry, at its installed path), not only core's components, the shell and the guest pages, and runs on vue-tsc 2.2.12 (installed as vue-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