mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-01 03:29:48 +00:00
4729d733bbb41fb415e5655e6e7a430daaa646ec
1684
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
26d843778e |
feat(dashboard): account dialogs as dashboard modals instead of windows (#4305)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
* feat(dashboard): account dialogs as dashboard modals instead of windows Edit profile, change username, change password, change email, create API token, and delete account in the Account tab (and change password in the Security tab) now open as dashboard overlay cards, like the share, properties, and confirm dialogs, instead of desktop UIWindows. They are centered cards on desktop and tablet and bottom sheets on phones. - dashboardOverlay.js: the overlay shell UIDashboardDialog already had (reveal, Esc/backdrop/close dismissal, focus trap, focus return), pulled out so the new form dialog shares it. It also pins the overlay to the visual viewport so iOS's keyboard can't cover a sheet's fields, and leaves Esc to a modal opened on top of it. - UIDashboardFormDialog.js: labelled fields, password show/hide, an aria-live status line that marks and focuses the offending field, a busy submit button, and a done state. - postUserProtected.js: the cookie-retry / provider re-sign-in / reauth flow the four account windows each carried, in one place. - Deleting an account is one dialog now (warning plus confirmation) rather than two stacked windows. - Touch: 44px controls, 16px inputs (no iOS focus zoom), no autofocus that pops the keyboard mid-animation, form buttons side by side on phones, safe-area padding dropped while the keyboard is up. - The replaced windows had no other callers and are removed with their tests, which are ported to the new modules. * fix(dashboard): a double-click that opens a dialog no longer dismisses it The second press of the double-click lands on the freshly mounted backdrop and closed the dialog straight away. Backdrop presses with a click count above one no longer count toward dismissal. * fix(dashboard): a failed form submit keeps focus in the dialog Locking the fields disabled the focused control, which drops focus to the page, so after an error Enter no longer retried and screen readers lost their place. The control that had focus gets it back once the fields unlock, unless the handler moved focus elsewhere in the dialog. * fix(dashboard): a form error keeps the field it names in view The status line scrolled the dialog body to the bottom, which on a short screen (a phone with its keyboard up) left the field being focused and marked invalid scrolled off the top. The body now scrolls back up just far enough to show that field. * fix(gui): a blocked re-sign-in popup fails instead of waiting forever openRevalidatePopup polled a popup that was never opened, so when the browser blocked it the account dialog stayed busy indefinitely. It now rejects straight away and the dialog says the sign-in didn't finish. * fix(dashboard): password show/hide reads as one action, not a pressed toggle The button flipped its label between Show and Hide password and also set aria-pressed, so assistive tech announced "Hide password, pressed" while the password was showing. It keeps the action label and drops aria-pressed. * fix(dashboard): screen readers hear a form dialog's success message The done state arrives as a new status region that already holds its text, which screen readers don't announce, so after a change succeeded they only heard "Done, button". The Done button that takes focus is now described by the message. * fix(dashboard): requesting an email change no longer sets the cached email The server only stages the new address until the emailed link is followed, but the dialog wrote it into window.user.email straight away. That cached a wrong address for the session and kept refresh_user_data from updating the Account tab once the change was confirmed, since the two already matched. * fix(dashboard): form dialog labels drop the global label shadow style.css's bare `label` rule gave the field labels a white text-shadow, which ghosted them on the dark theme, and an extra 3px under each label. * fix(dashboard): dialog inputs keep a 1px border when focused style.css's global input :focus rule still set a 2px border width, so a dialog field's text shifted by a pixel each time it took focus. The focus ring is the box-shadow; the border stays 1px. * fix(dashboard): phone dialog inputs stay 16px instead of jumping on focus On phones style.css's .device-phone input rule outranked the dialog's 16px size at rest, so fields drew at 17px and shrank to 16px the moment they took focus. * fix(dashboard): temporary accounts can delete themselves again The server reports oidc_only for every passwordless account, temporary ones included, so the shared user-protected helper sent a temporary account into the linked-provider sign-in, which has no URL for it, and the deletion never went out. Temporary accounts post directly, as the route admits them without credentials. |
||
|
|
63370fcfa5 |
fix(dashboard): mobile and tablet review — layout, touch, safe areas (#4303)
* fix(dashboard): give narrow Files listings the two-row layout The two-row rows (name, then size and date) were keyed to a phone-width viewport, so between 481px and ~930px, where the sidebar and folder column take their share, the fixed size and modified columns squeezed names to a few characters. The layout now switches on the listing's own width with a container query, which phones still meet and wide listings never do. * fix(dashboard): wrap the Files toolbar onto two rows in narrow listings On tablets the nav buttons, crumbs and actions sat on one row wider than the listing, so the sort, view, new-folder and upload buttons ran off the right edge. Narrow listings now use the phone's two-row toolbar (crumbs above, buttons below); only the menu-button clearance stays phone-only. * fix(dashboard): keep a deep folder path on one row with the current folder in view A long path wrapped the crumbs over several lines on top of the column headers and pushed the toolbar actions off-screen; where the crumbs already scrolled (phones), the row opened at the root, hiding the current folder. The crumbs now scroll within their row and are scrolled to their end on render and when the tab is shown. * fix(dashboard): end clipped file names in the list with an ellipsis With the name column auto-sized, which is always the case on phones and tablets, a long name was cut off mid-letter with no sign it continued. * fix(dashboard): keep the Files selection bar under the drawer and sheets The bar's visible state set z-index 99999999999999999 (clamped to the maximum), so in phone select mode it drew over the open navigation drawer, covering the account button, and over the share sheet it opens. Its base z-index already clears the rows and the footer. * fix(dashboard): let the last Files rows scroll clear of the phone selection bar In phone select mode the list's bottom padding only cleared the footer, so the last row stayed behind the floating action bar even fully scrolled. * fix(dashboard): keep the phone Files footer and selection bar above the home indicator The installed app runs edge to edge, so the footer's item count sat under the iPhone home indicator. The footer now grows by the bottom safe-area inset, and the list padding and floating action bar move up with it, as the Apps tab and the dashboard's bottom sheets already do. * fix(dashboard): clear the home indicator in the item properties sheet On phones the sheet sits flush with the bottom edge, and unlike the share sheet it mirrors, its body didn't pad for the bottom safe-area inset, so the last details scrolled under the iPhone home indicator. * fix(dashboard): keep the app feedback sheet's buttons above the home indicator As a phone bottom sheet it sits flush with the bottom edge, so Cancel and Send ended under the iPhone home indicator. * fix(dashboard): size the touch context menu as a card on tablets The touch menu took 90% of the viewport, which on an iPad meant a ~920px strip with its labels far from the row they belong to. It now stops at 400px, which every phone stays under. Its height is also measured after the width is set, so the clamp to the viewport uses the real height. * fix(dashboard): let the Apps modals scroll in a short viewport The Add-an-app and uninstall cards had no height cap and their overlay centers without scrolling, so on a landscape phone with the keyboard up (the viewport resizes for it) the request form's title and buttons sat off-screen with no way to reach them. * fix(dashboard): align Account and Security with the other tabs on phones Below 768px their wrapper added a 16px gutter inside the content area's own, so both tabs sat 32px in while Home, Apps and Usage (and the menu button above them) sit at 16px. The extra padding stays for Apps, whose content area has none. * fix(dashboard): keep Home's recent apps inside their card on tablets Each recent app was a fixed 200px inside 1fr columns, whose minimum is that width, so in the half-width card on a landscape iPad the second column ran past the card's edge and its name was cut off with the ellipsis out of view. The rows now shrink with their column. * fix(dashboard): keep every Files folder reachable in a short viewport Trash was fixed to the bottom of the viewport, so in a landscape phone or a short window it sat on top of Public and Videos, while Shared ran below the column with no way to scroll to it. The folder column now scrolls, and Trash sits at its foot in the flow: in the same spot when there is room, after Shared when there isn't. * fix(dashboard): don't leave Account and Security buttons highlighted after a tap Their hover background wasn't limited to hover-capable pointers like the rest of the dashboard's hover styles, so on touch devices a tapped button (Change Password, Manage Sessions, ...) stayed shaded after its dialog closed. * fix(dashboard): give the Files row menu button a full-size touch target On phones the row's '⋯' is 40x23, right beside a row whose tap opens the item, so a slightly-off tap opened the file or folder instead of its menu. In the two-row layout it now has a 44px square hit area on touch screens. * fix(dashboard): enlarge the Files back, forward and up touch targets They were 28px squares on touch screens. On coarse pointers they now take 36px with a matching negative margin, so the toolbar layout is unchanged. * fix(dashboard): size Home's recent apps card to its apps on phones Its 310px minimum height matches the square welcome card beside it on wider screens. In the single-column layout the welcome card drops its square shape but the minimum stayed, leaving a block of empty card under a short list of recent apps. * fix(dashboard): stop clipping app names' descenders on phones and small tablets Below 768px the tile height budgeted 52px icon + 10px margin + 12px for a label line that is 15.6px tall, so the bottoms of g, p and y were cut off in every app name. The tile now has the full line (78px) and the row gap gives back the 4px, so tiles sit exactly where they did. * fix(dashboard): show Usage amounts in full on phones Every Usage table cell ellipsized, numbers included, so on a phone a token count like 12,345,678 read as "12,345...". Below 480px the units and cost columns now keep their full width and the resource name is the one that truncates. Wider layouts are unchanged. * fix(dashboard): stack Home's recent apps when their card is narrow In the two-column Home on a portrait tablet (iPad Air and Pro 11 at 820-834px, or 769px windows) the Apps card is about 200px wide, and two columns of recent apps cut every name to a letter or two. The list now switches to one column on the card's own width; 1024px and wider keep two. * fix(dashboard): fit two grid tiles a row in narrow Files listings Grid view's 170px column minimum left one 330px tile per row on 360px phones (and in a tablet's narrow listing), so barely two files fit on screen. Narrow listings now use two equal columns, and the tile's icon box shrinks with the tile instead of keeping its fixed 150px. * fix(dashboard): align the Shared and Trash rows' menu button on phones Item rows get position: relative from jQuery UI draggable, but the Shared and Trash rows above Home's folders aren't draggable, so their '⋯' was positioned against the whole list and sat 3px off from every other row's. |
||
|
|
afad53d3f4 |
feat(gui): add Filipino and restore the language menu (#4263)
The desktop built the language list and never placed it on the user menu, so a saved locale could not be chosen. Register Filipino and show that list. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
8b8dd19eae |
fix(teams): refuse to boot when teams_enabled has nothing registering Teams (#4300)
* fix(teams): refuse to boot when teams_enabled has nothing registering Teams Core's seat checks (team 2FA, seat password reset, email and username changes, self-delete, the signup placeholder claim) read the team store and skip when it is absent. With `teams_enabled` on and no extension registering the store and service, the server now fails at boot instead of serving without those checks. - whoami no longer string-matches the extension proxy's "missing property" error: with the flag on, the store is always there. - Tests that turned the flag on without Teams drop it; the puter.js harness sets it only under the `teams` capability. - Drop a stale services/index.ts comment. * refactor(gui): drop the Teams console styles, which ship with the console now |
||
|
|
1627650abc |
refactor: make teams an optional feature (#4248)
release-please / release-please (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
* refactor: let core run without a teams registration
Teams becomes optional: `services.team` and `stores.team` may be absent, and
core stops assuming either is there.
Seventeen call sites across share, signup, account deletion and the homepage
now degrade instead of throwing: a team recipient that cannot be resolved is
"no such recipient" rather than a fault, a group share folds in nothing, and
account deletion stops asking what a seat was charged for. This generalises
the pattern `gates.ts` and `SocketService` already used.
Teams' own files take a private handle to their store and service, so the
feature's code stays free of optionality that only core needs.
No behaviour changes where Teams is registered.
* refactor: put a contract between core and teams
`core/teams.ts` holds the shapes core reads and the calls it makes, and the
registries declare the pair as provided rather than built in. The service,
store and controller are registered by whatever runs the feature.
Typing them properly turned up eight AuthController call sites the previous
commit could not see: the controller reads its stores from the built-in
registry, so an absent `team` fell to an index signature and typed as
`unknown` instead of erroring. They now go through the same optional path as
the rest.
The feature's own tests move with it. The cases in core suites that build a
team to exercise a core rule still need relocating, so those suites are red
here.
* test: stub core's seat rules, rehome the rest with the feature
A core suite can no longer build a team, so the rules that refuse a seat get
`testFixtures/seatStub.ts` instead: it answers the contract, which is all
those rules read, and puts the registry back when the test ends.
Seventeen cases move that way. Four could not: group shares and a seat's 2FA
enrolment over HTTP need the real feature and the `/teams` route, so they go
with it, as does the whole of `GroupGrants.test.ts`.
Two whoami cases asserted a seat's team through `extension.import`, which
resolves the registry rather than the stores object. They now assert the
absent case, and the behaviour they covered is asserted against
`buildUserDetails`, which takes its stores as an argument.
Suite: 12252 passed, 38 skipped.
* refactor: take the teams console out of the built-in tabs
`TabTeams`, `teamsConsole` and `teamActionIcons` are no longer built in. The
dashboard already invites a tab through its `dashboard-will-open` event, so
whatever provides Teams contributes the console the same way.
Its 113 strings go with it and register into the shipped English dictionary
at load, so `i18n('teams_*')` resolves as it did. Only `en.js` ever carried
them, so no translation is lost.
Three modules stay, because things other than the console use them:
`teamBadge` (the dashboard shell), `orgSeat` (Home, Account, Security) and
`shareTeams` (the share picker). They already render nothing when the
account has no team. `teams_account_of` stays with `teamBadge` for the same
reason, and the `share_*` and `plan_team_*` strings stay with their callers.
The GUI builds clean with no console at all, and builds it in when the tab
is contributed.
* docs: seats past the owner's current limit get no allowance
|
||
|
|
0760530d0c | feat(gui): edit display name and bio from the Account tab (#4293) | ||
|
|
3bce3b056e | misc: hardening (#4290) | ||
|
|
f02b4a6b73 |
feat: tell apps when a launched item is a folder; Open with AI above Open With
A folder opened with an app (Open With, Open with AI) reached puter.js as a file: the GUI never forwarded the signature's fsentry_is_dir, so the launched FSItem always had isDirectory false. The GUI now appends puter.item.is_dir, and puter.js reads it in onLaunchedWithItems and the onItemsOpened launch replay. Both now share one URL-to-FSItem builder. "Open with AI" moves directly under Open, above Open With, in the desktop and dashboard file menus. |
||
|
|
66093e78f3 |
feat(gui): "Open with AI" in the dashboard Files menu, opening like Open With (#4289)
"Open with AI" now launches the `ai` app with the item the same way
Open With launches any app: launch_app with file_path/file_uid, so the
app gets a signed launch item via puter.ui.onLaunchedWithItems. It
replaces the custom `ai:openFsEntries` postMessage, which went to an
existing AI window.
- The helper moved from UIItem into helpers/openWithAI.js, shared by the
desktop item menu and the dashboard Files menu (generate_file_context_menu).
- A shortcut opens its target.
- Dropped from multi-select menus, since a launch opens a single item.
- Renamed open_in_ai -> open_with_ai ("Open with AI") and removed the
unused ai_app_unavailable string.
|
||
|
|
53e6e3179b |
refactor(gui): remove the AI side panel and its desktop button (#4286)
The desktop no longer launches the `ai` app at boot. This removes the `is_panel` window mode, which existed only for that launch, and the toolbar/taskbar/window/popover offsets tied to it. It also removes the desktop sparkles button that showed the panel, and the `silent_on_failure` launch option, whose only caller was the boot launch. "Open in AI" in the item context menu still launches the app as a normal window. |
||
|
|
1a9e43f5e5 |
fix(dashboard): adversarial review — bugs, a11y, perf, i18n (#4285)
* fix(dashboard): size the Notifications sidebar row like its neighbors The bell button's font: inherit overrode the sidebar item's font-size (16px instead of 14px, 15px in the drawer), and its icon wrapper stayed 18px while the drawer grows nav icons to 20px. * test(dashboard): scope the directory-note assertion to the directory row The 2FA row reuses .teams-directory-note, so the unscoped selector read both notes and the test failed. * fix(dashboard): only treat a picked file as a new avatar while the avatar picker is open The Account tab listened for every file_opened on the dashboard window, so choosing an image for a web link's Change Icon in the Files tab opened the profile-picture crop modal, and saving it replaced the avatar. * fix(dashboard): let dialogs over the Files tab keep their keyboard The Files keydown handler ran under an open confirm, so pressing Enter on the focused Delete button of the permanent-delete prompt opened the trashed item instead (stacking a second alert), and arrows or Delete acted on the rows behind the dialog. * fix(dashboard): keep Apps grid keys away from an open context menu Choosing a tile's menu item with Enter (e.g. Open in new tab) also launched the focused app in place, and arrows moved tile focus while they walked the menu. * fix(dashboard): Escape on a menu or modal inside an app folder keeps the folder open The menu's and the uninstall modal's Escape handlers removed them before the folder's listener ran, so it saw nothing above it and closed the folder with the same keystroke. * fix(dashboard): a tap on the drawer or sheet scrim only dismisses it Outside-press handlers closed the mobile drawer and the notifications sheet on touchstart/pointerdown, which took away the scrim's hit area before the click, so the tap also landed on the app tile or file row underneath. * fix(dashboard): let the sidebar nav scroll on short viewports The nav clipped its overflow, so in a landscape phone's drawer (or a short desktop window) Usage, Account, and Security fell below the fold with no way to reach them. * fix(dashboard): keep the mobile drawer full width when the desktop sidebar is collapsed The remembered collapse outranked the drawer's width rule, so below the drawer breakpoint it opened as an unlabeled 50px icon strip, and the toggle that would undo it is hidden there. * perf(dashboard): stop refetching account data each time focus leaves an app Home's window focus listener fired whenever focus came back from an app's iframe, so every click from an app to the dashboard re-pulled whoami, subscription, storage, and usage (5 requests), on any tab. It now only refreshes after the page itself lost focus to another window. * fix(dashboard): show a seat's team name unescaped in the plan card i18n escapes its arguments for HTML, but the note is set with .text(), so a team called "Bob's & Co" read "Managed by Bob's & Co". * fix(dashboard): stop double-escaping app names in uninstall alerts UIAlert escapes its message, so an app titled "Tom & Jerry" read "Tom & Jerry" in the uninstall-failed and sessions-not-ended alerts. * fix(dashboard): roll back an uninstall whose revoke failed puter.perms.revokeApp resolves with error set instead of rejecting, so a failed revoke still removed the tile, marked the app removed for later sessions, and ended its sign-in, while the app kept its permissions and the rollback and error alert never ran. * fix(dashboard): read a team account's suspension when its name matches an Object member The per-account state map was a plain object, so a username such as constructor or toString was already "in" it, its audit entries were skipped, and a suspended account showed as active with no Delete. * fix(dashboard): update the profile card and sidebar name after a username change Neither carried the .username class that the rename refreshes, so both kept showing the old name until reload. * fix(dashboard): ask how to restore a trashed item whose name is taken Restore moved without overwrite or dedupe, so when the original location already had an item of that name the move 409'd and nothing happened: an uncaught error from the item menu, a console line from the others. All three restore entry points now share one path that offers the Replace / Keep Both prompt the paste flow uses, alerts on other errors, and updates the footer and empty notice once the rows are gone. * fix(dashboard): refresh a renamed row's name, icon, and position in the Files tab The Files tab passed rename_file a callback it never accepted, so a renamed row kept the desktop's 20-character truncation even in a wide column, kept its old type icon, and stayed out of sort order. rename_file now takes an optional onDone (carried through undo), which the Files tab uses to fit the name to its column, refresh the icon and type, and move the row into place. A cancelled rename no longer truncates the name either. * fix(dashboard): drop Publish and Properties from the Shared view's menu The Shared view is a query, not a folder, so both acted on the virtual shared path: Properties showed an unknown error and Publish opened a dialog for a path that doesn't exist. * fix(dashboard): close the Files image preview on Escape and when leaving the folder Escape cleared the selection but left the Space preview open with nothing selected, the preview stayed over a different folder after navigating, and a preview whose URL arrived after it was dismissed or replaced still opened. * fix(dashboard): don't move Files history for a navigation that's dropped renderDirectory ignores a navigation while another folder is loading, but every caller had already pushed or stepped history, so after a click during a load the listing and history disagreed and the next Back or Forward seemed to do nothing. History now only moves when the render will run; back/forward steps share one helper. * fix(dashboard): reopening the current folder adds no Files history entry Clicking the folder already on screen (e.g. Home in the sidebar) pushed a duplicate entry, so the next Back reloaded the same folder. * fix(dashboard): show installed apps when the recommended list fails to load get-launch-apps sat in the same Promise.all as the installed apps and its body was parsed without a status check, so a network error or a proxy's HTML error page replaced the whole grid with "Failed to load apps". A failed list now falls back to the last one that loaded. * fix(dashboard): fit the app uninstall dialog on narrow phones The card is mounted outside the dashboard's border-box rule, so its 320px min-width plus padding came to 370px and was clipped on 360px screens; a long unbroken app title also overflowed it. Desktop sizing is unchanged. * fix(dashboard): keep a new team account's one-time password on screen until dismissed The credential box lived only in the DOM inside the panel that every refresh repaints, so revisiting the Teams tab or any later action wiped a password that can't be retrieved again. It is now kept in state, for the team it belongs to, and drawn with the panel. * fix(dashboard): reload the Teams tab after a change made while it was loading refresh() handed back any load already in flight, so an action or team switch made during a revisit's background load awaited data read before the change: a toggled setting snapped back, and a team switch could paint one team's roster next to another's record. A change during a load now queues one more load, the stale one skips its paint, and revisiting the tab doesn't start a second load while init's is still running. * fix(dashboard): send one re-enable request for a double click in Teams Re-enable has no confirm dialog to absorb a second click, and nothing held the action while it ran, so a double click enabled the account twice (two audit entries). A suspend or re-enable already in flight for an account now ignores further clicks. * fix(dashboard): revert the avatar and say so when saving a new photo fails The crop's Save showed the new picture everywhere and revealed Remove without waiting for the upload, and update_profile swallows failures, so a failed save looked saved until reload. The save is now awaited: a failure puts the previous picture back with an error hint, and Remove waits for the upload so it can't race it. * fix(dashboard): hide the Usage tab's plan button for team seats A seat's plan belongs to its team, so Home already hides its upgrade link; the Usage tab still offered Upgrade, which only reaches a refusal. * fix(dashboard): don't drop Home's forced usage refresh during a running load loadUsageData handed any caller the load already in flight before looking at force, so a subscription change (or a return from the billing portal) during a routine load was answered with data read before the change, and the plan card stayed stale until the next refresh. A forced call now runs once more after the running load. * fix(dashboard): activate Apps when the URL names an unknown tab An unknown hash rendered Apps as the active tab but skipped its activation, so the grid kept the generic content padding and Apps' onActivate never ran. * fix(dashboard): make the sidebar user menu reachable from the keyboard The user button was a div with no tabindex, so Log Out, account switching, and Contact Us were out of reach without a pointer. It is now a button (rendering unchanged), with aria-haspopup. * fix(dashboard): name the sidebar's collapse and drawer buttons for assistive tech The collapse toggle held only two unlabeled images, so screen readers announced an unnamed button; it now carries a translated label and aria-expanded that follow its state. The drawer's open/close labels were hardcoded English and now go through i18n. * fix(dashboard): make Home's recent apps reachable from the keyboard Each recent app was a click-only div, so keyboard users couldn't open one. They are now buttons with a focus ring; the icon is decorative since the title names the button. * fix(dashboard): name the Security tab's 2FA switch for assistive tech The checkbox sat in a text-less label, so screen readers announced an unnamed checkbox. It now carries the translated Two Factor Authentication label, as the Teams switches do. * fix(dashboard): sort the Usage table from the keyboard The sortable headers were click-only th cells. Each now holds a button, carries aria-sort, and keeps focus across the re-render a sort causes. The Resource/Units/Cost headers go through i18n. * fix(dashboard): don't rebuild the Files listing under an open menu Returning to the browser tab re-rendered the folder even with a context menu or action sheet open, so its actions then targeted detached rows: Rename did nothing, Restore faded a row that was already gone. The refresh now waits while a menu is up, as it does for a rename. * fix(dashboard): point the Files sidebar's Documents at the current home The entry read window.documents_path, which globals.js sets once from the cached user and nothing updates; login and username changes keep window.docs_path current instead. After a username change, Documents pointed at the old home and showed "This folder couldn't be opened", even after reloading. * fix(dashboard): let only the newest share listing render in the share dialog Each action ends in a refresh, and refreshes from overlapping actions (or the opening load) could finish out of order, so an older listing landed last and undid what the user just did, e.g. the link control snapped back to restricted after turning the link on. Older refreshes now drop their result. * perf(dashboard): load a team's record, roster, and plan in parallel loadSelected awaited the audit log (every page of it), then the members, then the plan, on every visit to the Teams tab. They don't depend on each other, so they are now fetched together. * fix(dashboard): keep keyboard focus on the Teams control that was used Every Teams action repaints the whole panel, which dropped focus to <body>: a keyboard user toggling a switch or suspending an account was sent back to the top of the page. The repaint now refocuses the same control, or the same account's new action when the old one is gone. * fix(dashboard): give the app uninstall prompt dialog semantics and focus The prompt had no dialog role or label and never took focus, so keyboard and screen-reader users stayed on the inert grid behind it. It is now a labelled modal dialog that focuses Cancel on open and returns focus to the app's tile when dismissed. * fix(dashboard): keep the add-an-app tile when there are no apps With no apps at all, the empty state replaced the whole grid, add tile included, so a new user on an instance with no recommended apps had nothing to act on. The empty line now sits above a grid that keeps the tile, as a search with no matches already does. * fix(dashboard): don't rebuild the Apps grid under an active drag An uninstall finishing (or a failed one rolling back) re-rendered the grid even while another tile was being dragged, replacing the dragged tile with a fresh copy: the copy showed under the ghost and the drop was lost. Renders asked for mid-drag now wait for the one the drop does; the folder eject, which rebuilds the grid as part of the drag, still can. * fix(dashboard): return focus to the drawer toggle when the mobile drawer closes Closing the drawer (Escape, the close button, a tab pick) left focus on controls that had just become hidden, so it fell to <body>; closing the notifications sheet opened from the drawer tried to refocus the bell inside the closed drawer. Both now land on the drawer toggle. * fix(dashboard): truncate Files names between graphemes, with a binary search The middle truncation sliced UTF-16 code units, so at many widths a long name with emoji showed a half surrogate (rendered as a replacement character) or a broken joined emoji. It also measured one more character per step, a measureText call per character of every long name on each render and column resize. The cut now falls between graphemes and is found by binary search; output for other names is unchanged. * fix(dashboard): translate the Home tab's hardcoded English The tab label, the recent-apps empty state, card subtitles, the plan badge and trial wording, the upgrade/manage links, and the usage cards' "Used" / "of" lines were English literals. They now go through i18n, with placeholders where values are interpolated. * fix(dashboard): translate the sidebar's remaining English The Apps, Files, and Usage tab labels, the user button's fallback name, and the user menu's Developers item were English literals; they now use i18n keys like the other tabs. * fix(dashboard): translate the Usage tab's hardcoded English The subtitle, the "used of" meter lines (now the existing used_of key), Usage Details, the plan button, Show more/less, and the details error were English literals. * fix(dashboard): translate the Account and Security tabs' hardcoded English Both subtitles, the delete-account description, the sessions hint, the username fallback, and the avatar picker's window title were English literals. * fix(dashboard): translate the Apps tab's hardcoded English The uninstall prompt and its failure alert, empty and no-match notices, pager and folder-dot labels, the search placeholder, the edit toggle, and the tile menu's Open in New Tab / Quit / Uninstall were English literals. * fix(dashboard): translate the Files tab's hardcoded English The folder sidebar, footer counts (now the existing item-count keys), sort and view menus, upload status and errors, the empty and can't-open notices, the reserved-name alerts, the move error, and the loading text were English literals. * fix(dashboard): translate the action sheet's Back row and match delete in any language The touch action sheet styled an item red when its label contained the English word "delete", so Delete lost its danger styling in every other language; it now compares against the translated Delete labels. Its submenu Back row and the properties panel's Directory/disassociate labels were English literals. * fix(dashboard): make the Files sidebar, crumbs, and nav arrows keyboard-operable The folder sidebar, the breadcrumbs, and the back/forward/up arrows were click-only li/span/img elements, out of the tab order. They now take focus, carry button/link roles (the arrows with labels and aria-disabled), answer Enter and Space, and show a focus ring. The rows' Enter/Space shortcuts also no longer swallow those keys from a focused control: with rows selected, Enter on Upload opened the selection. * perf(dashboard): read grid name widths before writing any in the Files tab fitItemNames read each tile's name width right after writing the previous name, forcing a layout per row. Reading them all first, then writing, cuts switching 400 files into grid view from about 300ms to 170ms; the rendered names are identical. |
||
|
|
54f542ace4 |
fix(gui): refresh Start menu apps without blocking cached rendering (#4244)
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
* fix(gui): refresh Start menu apps without blocking cached rendering * docs: remove desktop Start menu limits from SDK reference * fix(gui): drop the 1 MiB cap on the launch-apps refresh Recent entries carry each app's raw icon column, which can still hold a data URL of up to 5 MB, so a large icon tripped the cap on every request, including the one at startup, and left the Start menu with no apps. Replace the cap's test with one for the unlaunchable-app filter. --------- Co-authored-by: Daniel Salazar <daniel.salazar@puter.com> |
||
|
|
18b96d529c | fix: misc scan fixes (#4270) | ||
|
|
f146da16d1 |
fix(gui): drop deleted apps from the start menu (#4243)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
The start menu read a session-cached launch-apps list and only refetched when it was empty, so an app deleted after page load (for example in Dev Center) stayed in Recent and Recommended until a reload. Refetch on every open, and filter out entries the backend marks unlaunchable, so a reclaimed or inaccessible app leaves the list too. Apply the same filter to the dashboard's Recent apps. Adds helpers/launchAppsList.js with filterLaunchableApps and its unit test. |
||
|
|
9ad7b99607 |
fix(puter-js): handle file picker cancel message (#4257)
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
|
||
|
|
fa62b3fb76 |
move template menu entries into a tested helper (#4258)
Builds the "New > User templates" entries in helpers/templateMenuItem.js and covers the label with tests, including HTML encoding of both the extension and the name. |
||
|
|
2b254b8892 |
fix: misc bugs (#4251)
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
|
||
|
|
8faff74793 |
PUT 2057 and PUT 2068 (#4245)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
|
||
|
|
c6c0dd5e29 |
fix: bound how many accounts a team owner can provision (#4223)
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
* feat: bound provisioning by what an owner may mint, not only what is live Seat and team caps express how much may exist at one time. This adds the other half of the rule: how much an owner may create over a window, and the checks a provisioned account answers to. - Provisioning puts each seat through `SignupService.runSignupGate`, the same extension-owned gate a signup answers, and refuses what it blocks. It fails closed, since a check that could not run is not a check that passed. - A per-owner budget over a rolling window, sized to every seat the owner may hold live across every team they may own, times the churn allowed on top. It is spent on creation and is not returned. Keyed on the owner's uuid so a recycled id cannot inherit someone else's spend, and charged after validation so a taken username costs nothing. * refactor: tighten the provisioning gate and budget after review - The gate runs before the seat-cap lock. A listener has no deadline and the lease is ten seconds with nothing to renew it, so the lock must not be held across one. - `failOpen: false` rethrows rather than returning false, so a backend outage is answered 503 instead of escaping the handler. - An address goes through `validateEmail` first, as it does on every other path that creates an account. - The gate always runs; a request only enriches what it is told, so one being absent is no reason to skip it. - The gate is told it is looking at a seat, whose address is optional. - A refusal carries the gate's own code and request code, as a blocked signup does, so support can correlate it. - Restored `provisionAccount`'s docblock, which an insert had displaced. Tests: the spy restores in `finally`, and the cap overrides are taken after the fixtures, so one failure cannot cascade through the file. * fix: show a provisioning refusal as a rule, not a failure The console special-cases the seat cap and sends everything else to "Something went wrong", which reads as a fault rather than an answer. The provisioning budget refuses the same way the cap does, so it is presented the same way. |
||
|
|
ff815f730d |
fix: harden token delivery, session revokes, app identity and fs scoping (#4229)
release-please / release-please (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
- relay sign-in popups take the opener origin from the referrer only - session revokes read the primary, batch per user, and tombstone caches in every region - wisp relay tokens bound to the minting session; verify rechecks the account - hosted origins keep app identity with the site owner; deleted app uids recorded in deleted_apps - subdomain cache tombstoned on delete, update, reclaim and app/account cascades - email lookups require an exact address match after the SQL lookup - workers deploy only from files the deployer owns - share_app_data:false enforced at ACL time, plus copy/move/rename/share/recursive listing - kv remove returns no value without read; kv:expire op grant usable - fs errors name the entry instead of the owner's path - shared-by-me drops rows a delegate no longer has authority over - private apps can't move index_url onto another user's origin - GUI: default apps saved and launched by uid; save picker accepts only copy/move; permission dialog waits for focus; Publish as Worker hidden on others' files |
||
|
|
62270ff9aa | Delete worker sessions when workers are deleted, don't treat sessions from deleted workers as still valid. (#4163) | ||
|
|
b52fc01712 |
perf: cut godmode app launch latency (cached token renewal, launch reuse) (#4221)
* perf(auth): renew godmode app tokens from cache instead of the primary Every godmode launch read its token row from the primary and rewrote its expiry, a few cross-region round trips per launch far from the primary. - A cache pointer names the row per (user, web session, app). The row is read through the uuid cache token auth uses and re-checked (owner, session, app, revoked, expired, max age); anything else falls back to the primary read. Revoking the row drops the pointer. - The expiry write is skipped when it would move by under an hour. A renewal that hits a revoked or lapsed row mints a new one instead. - The response carries the row's actual expires_at. * perf(gui): reuse a just-minted godmode token for repeat launches A launch of a godmode app reuses the token this page minted for it within the last ten minutes under the same desktop session, instead of asking again. The cache is dropped when an app reports its token dead, after a session revoke from the session manager or an uninstall, and across tabs via a BroadcastChannel that carries token row ids, never tokens. A mint in flight during a drop is not remembered. |
||
|
|
f37cb22d8a |
fix(gui): resize app content above mobile keyboard (#4165)
Add interactive-widget=resizes-content to the mobile viewport meta tag so the layout viewport shrinks when the soft keyboard opens. The app window's height chain resolves against the layout viewport, so the iframe folds up above the keyboard instead of clipping behind it. |
||
|
|
b8bb6292d6 |
Fix webdav locks issue and tighten GUI ipc rules for setWindow* (#4207)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
|
||
|
|
86cc3490f7 |
fix: settle which plan an org seat resolves to, and confirm a seat's address (#4139)
* fix: settle which default plan an org seat resolves to, and what counts as paid Default subscription resolvers ran in registration order, which is boot order: whichever layer registered first answered first. A seat is provisioned by a team and its address is never confirmed, so the seat resolver now states a priority and wins over anything a deployment infers from an address. The seat cap asked whether the owner's plan was outside the free set, which a default plan can be without anyone having bought it. Metering now reports whether an explicit subscription answered, and the cap rests on that. * fix: the review's findings on plan resolution `fromDefault` was read off which resolver answered, not off which policy applied. An explicit id naming a policy nobody registered falls through to the default one, and that read as bought. It follows the applied policy now. A deployment with unlimited metering has no explicit resolver, so every owner on it became unpaid and its teams dropped from the paid seat cap to the free one. Unlimited answers for what a paid plan would, so it counts as one. Also: the new constants had been placed between `FREE_SUBSCRIPTION_IDS` and its docblock, the register docstring still described first-wins, and prettier had reflowed an untouched licence header. * feat: a seat confirms the address its team typed in An address on a seat is the team's claim about someone else's inbox, and nothing asked the inbox. It is now demanded and sent a code, so the address counts for nothing until the seat hands it back. Resolvers that read a domain are the reason this matters. No address is still fine and demands nothing: the team is the trust anchor for the account, not for a mailbox it does not hold. `/confirm-email` allows an unconfirmed actor, so a seat can sign in with its temporary credential and clear the demand; nothing locks it out. * fix: clear the confirmation demand with the address it names `clearUnconfirmedEmail` releases a seat's address so the inbox's owner can sign up with it. The demand to confirm that address has to go at the same write: the gate reads `requires_email_confirmation && !email_confirmed`, and once the address is gone there is nothing left to send a code to. * fix: keep a seat's address correctable, and only ask what can be answered Review of the confirmation demand turned up two ways a seat could be left holding one it had no way to meet: - With no mail transport configured, the code is never sent and a resend cannot reach the seat either, so the demand stood with nothing able to lift it. The seat is now gated only when a code can actually travel. - An address the owner mistyped gated the seat on an inbox nobody could read, and the gate itself closes `/user-protected/change-email`, the route the seat would have fixed it by. `updateSeatEmail` lets the owner who typed the address retarget it, and refuses once it is confirmed, when it belongs to whoever proved it. Also resolves the owner's plan sequentially, so the second read takes the cache the first wrote and the pair cannot come from two resolutions, and restores the licence header a jsdoc reflow had rewrapped. * fix: let the confirm-email window open before the profile has loaded Booted from a stored token, the window renders before `window.user` is populated, and reading `.email` off null threw inside the window itself -- so the one screen that could have asked for the code never drew, leaving a blank page. Reproducible without a team: sign up, then load the GUI again before confirming. The address is now encoded rather than interpolated raw, and both forms of the line are i18n keys; it was hardcoded English before. |
||
|
|
ae234ffcc2 |
fix: only sign-in and picker popups hand the opener a token (#4147)
* fix: only sign-in and picker popups hand the opener a token Popup actions are an allowlist now: sign-in popups (no action, sign-in, login, signup) deliver at boot, pickers deliver with the pick, and request-permission keeps its exchange. Any other action runs no exchange and posts no token. The consent check in postAuthActions applies to every action that delivers at boot instead of only no-action and sign-in. * fix: a dismissed account picker hands the opener no token |
||
|
|
2ae2e093e7 | keep archive entries inside the extract folder, encode template labels (#4145) | ||
|
|
4f3a524ace | fix: credit usage gui (#4140) | ||
|
|
bb829885ef |
fix: a picker popup hands its opener a token once the user has picked (#4133)
* fix: a picker popup hands its opener a token once the user has picked
A picker opens because the site asked, not because the user agreed to
anything, so the hand-off now waits for the answer the popup was opened
to get. The exchange still runs at boot — the app row and host_app_uid
are what the dialog is opened against — but the token travels with
fileOpenPicked, directoryPicked or fileSaved. Cancelling sends nothing.
deliversTokenAtBoot is the one gate every boot-time hand-off reads, so
the plain exchange, the cross-origin-isolated /login/set path, temp-user
creation and manual signup all follow the same rule. A failed exchange
still reports and closes, having nothing to defer.
* fix: the review's findings on the picker hand-off
Double-clicking a file answers the opener from openItem.js, not from the
UIWindow button, so that pick returned the item and no token. The
deliverer is a global now and every path that answers an opener calls
it: both UIWindow handlers, openItem, and the save flow.
A failed exchange posted `{success: false, token: null}`, and the SDK's
handler sets that as readily as a real token, so a picker that failed
wiped whatever the site already held. The failure now goes only where a
boot-time token would have.
|
||
|
|
d1d55a6bc3 |
SDK auth plumbing: cache scope, dedupe keys and the token listener (#4112)
* fix: pin the token sender, scope the grant flag, and stop two stragglers
Five of the SDK auth-plumbing items.
**The global `puter.token` handler pinned origin but not source.** Its three
siblings all pin both, and each says why: the GUI origin is shared by every
frame on that domain, so origin alone identifies a domain, not a sender.
`signIn` pins its popup and binds `msg_id`, the consent dialog pins its popup,
and the reauth wait pins the embedder. This one pinned the embedder only when
`env === 'app'`. Since `authenticateWithPuter` now hands off to `signIn`, which
adopts the token itself, the embedder is the only sender this handler is left
serving, so it pins in every environment.
**`is_grant_user_app_permission` was a request-wide flag held across an await.**
It tells the `app-root-dir` rewriter that a row is being written, and while it
is set that rewriter resolves to a real `fs:` permission instead of
`PERMISSION_FOR_NOTHING_IN_PARTICULAR`. Anything evaluating an `app-root-dir:`
permission beside the grant -- the grant route runs one under `Promise.all`
with fs work -- read the same flag. It now lives in a scope derived for that
call, so what the write needs to say about itself is not also said to its
neighbours. `runInDerivedContext` is new: `runWithContext` starts an empty
store, which would drop the `actor` the rewriter reads.
**`fs.stat` and `fs.readdir` deduped on parameters alone**, so two identities
in one realm could share a result. They now key on origin and token as
`os/user.js` does, which documents the reason.
**`signIn`'s relay poll never stopped.** Closing the popup settles the promise;
the loop kept asking `/login/wait` every second for the life of the page.
**`triggerReauth`'s five-minute timeout was never cleared**, so a successful
reauth left a timer to fire into a settled promise.
The concurrency test parks the rewrite and reads the flag while it is held --
my first attempt read before the flag was even set and passed either way.
* fix: scope the fs cache per identity, and retarget two paths that never ran
The remaining SDK auth-plumbing items.
**`puter._cache` outlived the identity that filled it.** `_clearAuthToken`
dropped the token and the stored session but left the cache and `whoami` as
they were, and its keys were raw paths -- `item:/alice/Documents` and a
`~`-relative key alike, identical whoever asked. A GUI user switch without a
reload could be served the previous user's stat or readdir. Keys now carry the
origin and token they were read as, built in one place so the writers, the
readers, the invalidations and the warm-up probes cannot drift apart, and
signing out drops the cache and `whoami` with the token.
**The driver permission prompt could not fire.** Both paths waited for a 200
carrying `{success:false, error:{code:'permission_denied'}}`. The driver route
answers `success: true` or throws, so a refusal arrives as a 403 -- the shape
the retry now reads. Its two tests synthesised the old envelope, so they passed
against a response the API does not send; they use the real one now, and the
prompt goes from never called to called once.
This is a behaviour change: a driver call refused for permissions will now
prompt, which is what the path was written to do. Happy to delete it instead if
the prompt is not wanted.
The half-written copy of the same idea in `utils.js` is gone -- it prompted for
one hard-coded interface and its retry was a `// todo`.
**The verification-gate single-flight was keyed on nothing**, so a phone-gated
request in flight beside an email-gated one took the email answer, replayed,
failed again and surfaced raw without ever asking. Keyed per gate: same gate
still shares one dialog, a different gate gets its own.
* test: ask the cache the same way the code does
The API suites built `item:<path>` keys by hand, so they stopped finding
anything once the real keys carried the identity they were read as. They go
through `fsCacheKey` now, which is the point: a test that spells the key itself
passes until the moment the spelling changes, and then fails for a reason that
has nothing to do with the behaviour it is guarding.
These run against a live server, so a plain `vitest run` does not reach them --
they were green locally and red in CI.
* fix: the review's findings on this branch
Eight, and three of them were regressions this branch introduced.
**Pinning the token sender to `globalThis.parent` in every environment broke
the case it was meant to protect.** On a top-level page `parent` is the page
itself, and the token arrives from a popup, so the handler stopped accepting
anything. The GUI posts `puter.token` to `window.opener` for the file pickers
too, not only for sign-in, and this handler is their only consumer -- a site
calling `showOpenFilePicker()` would have signed the user in, dropped the token
and run the following read with none. It now pins the embedder when framed and
a window this SDK opened when not; `UI.js` already tracked its picker popups
for the same reason, so that set is shared rather than duplicated.
**The GUI still spelled a cache key by hand.** `updateSubdomainsForItems` built
`item:<path>`, which no longer matches what the SDK writes, so its `get` could
not hit and its `set` wrote an orphan nothing reads and nothing evicts -- a
published website's badge quietly stopped appearing on a cached render. It goes
through `fsCacheKey` now, and that is the last hand-spelled key in the tree.
**Retargeting the driver permission prompt to 403 was wrong and is reverted to
the deletion the ticket asked for.** The route refuses with `forbidden`, not
`permission_denied`, so the retarget still missed it; meanwhile a driver that
raises `permission_denied` for its own reasons would have opened a prompt that
had nothing to do with the refusal. And the prompt asks for
`driver:<iface>:<method>` while the route checks `service:<driver>:ii:<iface>`,
so granting it changes nothing. The path cannot work as written.
The rest were pre-existing or latent:
`cacheWhoami_` wrote its answer without checking the token it was issued under
was still current, so a reauth landing mid-flight could repopulate `whoami`
with the previous identity. `_dropIdentityCaches` also left `whoamiCache_`,
which other call sites await.
`setAuthToken` now drops the identity caches when the token actually changes:
keys carry the token and the store is persisted with no TTL, so a rotation that
did not go through `_clearAuthToken` orphaned a whole key space with nothing to
reclaim it.
The verification-gate entry was registered after the body could already have
deleted it -- a synchronous throw stranded a resolved "not verified" in the map
for the life of the page. It registers first and clears on a microtask.
`signIn`'s poll is bounded as well as settle-checked: on a cross-origin
isolated page opened from a gesture, nothing watches the popup, so closing it
never settled the promise and the loop ran on exactly as before.
`runInDerivedContext` says in its docblock that callee writes do not survive;
only the one flag needs the scope, and a future caller should not expect to
hand a value back through it.
* fix: the review's findings on the SDK auth plumbing
**The relay bound stopped asking without saying so.** The deadline left the
`signIn` promise pending and its listener attached. On an isolated page opened
from a gesture the relay is the only thing that can settle it -- nothing watches
the popup there -- so a user who took longer than the deadline was left with a
promise that never resolved, and `authenticateWithPuter` queues every later
implicit-auth call behind it. It rejects now.
**Pinning the token source left `signIn`'s own popup untracked.** Only the
picker popups registered, so on a top-level page the global handler bailed on a
sign-in it had opened itself: `puter.onAuth` never fired, and
`puterAuthState.authGranted` was never set. `openAuthPopup` is the one place
auth popups are opened, so they register there.
**A closed popup is ours too.** Rejecting one that had already closed loses a
message from a popup that posts and then closes itself, which is why `UI.js`
tolerates the same case. Kept, with the set bounded instead.
**The gate key carried the factors**, so one route naming them and another not
opened two dialogs for the same gate. It keys on the gate, as `ctx.done` does.
**`fsCacheKey` left `~` unresolved**, so a writer and an invalidation spelling
the same place differently missed each other -- the drift the one key builder
was meant to end.
The dead `opts.permission` went with the path that used to read it.
Four workerd-runner failures are left: `ai > chat stream adapter supports
cancellation`, `kv > a GUI boot key written by another client reads fresh` and
two cache-warmer ones. All four fail the same way on `origin/main`, and the node
and browser runners pass.
* chore: one-line the comments added here
* chore: reflow the comments the removed retry cause left ragged
|
||
|
|
4ea93e6a73 |
fix(i18n): add GUI keys that rendered as raw key names (#4074)
The 2FA spinner showed "verifying", the files selection bar showed a "done" button, and two login and signup errors showed their key names, because i18n() returns the key itself when en.js has no entry for it. The login error now uses the existing login_email_username_required key, which already has translations. A test checks that every literal i18n() key in the GUI exists in en.js. |
||
|
|
47a1be7f70 | Rename "Add Existing Account" menu item to "Add Another Account" (#4124) | ||
|
|
b6dfc6f903 | fix: harden card verification (#4126) | ||
|
|
2e275a8f6b |
fix: godmode installation (#4116)
* fix: godmode installation * fix: godmode classifier * fix: vitest |
||
|
|
464f758a97 | fix: verification for sms (#4100) | ||
|
|
50ee0818ea | feat: trial cleanup (#4097) | ||
|
|
25f132b28d |
fix: address code review on the teams cleanup
Six findings, all real. `total` described rows the page withheld. The count ran over every live team the user belongs to, while the app-actor filter ran afterwards on the items only, so `GET /teams?includeTotal=true` from an app returned `items: []` with a non-zero `total` -- the exact thing the filter exists to prevent, and newly reachable because `includeTotal` was inert before this branch. The directory-open restriction moves into the query, so items, cursor and total all describe the same set. The GUI told owners the opposite of what the server now does: "we email the username and temporary password to it." An owner who believed it would dismiss the one copy of the password and strand the seat. Rewritten. `runWithConcurrencyLimit` rejects on the first error but cannot cancel the workers beside it, so a failed seat let up to seven others finish suspending and emailing "your team has been closed" for a team that was still live. The page now settles before the rejection is rethrown, which leaves a state the retry can resume from instead of an arbitrary one. My earlier claim that failure semantics were unchanged was wrong. `#disableSeatForDelete` returned `true` on both paths, so the boolean read as if it distinguished them. It returns nothing; the counter uses the settled results. `listTeamsForUser` had picked up a second stacked docblock -- the same defect this branch removes from `#cached` one file over. `reissueCredential` sent the provisioning mail, which after the credential change was a byte-identical duplicate saying nothing about the password it had just invalidated, while the GUI promises the member hears about it. It has its own template now. Regression tests for the count leak and the reissue notice; both confirmed to fail without their fix. |
||
|
|
a46eb15d5e |
Let the account's own API token reach the routes a privileged app needs (#4032)
* feat: let the account's own API token reach the routes it should
A privileged app is launched with the GUI session token today. Running one on a
full-access personal access token instead costs much less when it leaks, but
several routes those apps depend on refuse every access token, so the swap would
break them.
Admitted, each a read or a write the token's own HTTP reach already covers:
- the realtime socket handshake, which refused access tokens outright; FS
live updates and notifications stop without it
- GET /get-dev-profile and POST /profile, both puter.js surface
- GET /auth/list-permissions — a read of what was granted, not a grant
- GET /share/shared-by-me/apps and GET /share/audit, over grants
/share/shared-by-me already lists for the same credential
- POST /rao, which puter.js calls on every setAuthToken
Gated on `isAccountContext`, not the `full_access` claim alone, so a token with
an app anywhere in its chain is still refused and a socket for one never joins
the user room.
Left session-only: mint, session, 2FA, grant, team and billing, plus two the
list named. /share/blocks is a personal safety control whose only caller is the
desktop's Blocked Senders window. /app-feedback is reachable only from our own
GUI pages, and being unsubmittable programmatically on a user's behalf is the
property it was built with.
The new HTTP suite drives a real server with a minted token and fails on all six
admissions without the change; the block routes' existing metadata check already
pins their refusal.
* test: sort the two token shapes at the events handshake
* fix: carry the account-context invariant into the HTTP gate
`requireUserActorGate` admitted on the raw `full_access` claim while the socket
and /rao use `isAccountContext`, so one shape — a token carrying the claim with
an app in its chain, or an actor built without `makeActor` — would have been
refused in two places and admitted in the third. Unreachable today, since the
mint and the auth-time read both drop the claim when `app_uid` is present, but
it is the invariant this change states and it should hold at every enforcement
point.
The payout address goes back behind a plain session: `/get-dev-profile` is open
to the account's own token now, and the rest of what it answers is a developer's
own name and incentive-program status.
Also drops a cross-reference in EventsService to the socket posture this change
relaxed, and rewords the block-routes comment, which read as if access tokens
were admitted there rather than refused.
* fix: drop a revoked token's sockets, and read the dev-profile columns
Two gaps the review found, both reachable only because this branch lets the
account's own token hold a socket and call /get-dev-profile.
`#revokeAccessTokenTail` soft-revoked the session row and told no one, so a
revoked token kept its connection — and the account's `outer.gui.*` fan with it
— until the five-minute reauth sweep. Sockets now join a per-token room and a
new `auth.access-token.revoked` event drops exactly that room, so the account's
other tabs stay up. Session revoke keeps its account-wide eviction.
`/get-dev-profile` read `first_name`, `last_name`, `paypal` and the two
incentive flags off the user row, which carries none of them: the columns have
always been `dev_`-prefixed, so the endpoint answered nulls to everyone and
`puter.apps.getDeveloperProfile()` has never returned anything. Reads the real
columns, keeping the unprefixed name as a fallback for a deployment carrying
both. The payout address stays behind a plain session, so opening the route to
the account's token hands over a name and incentive status and nothing else.
Both fail without the fix: the socket test waits five seconds on a connection
that should already be gone, and the profile test reads back a seeded row.
* fix: close /open_item to tokens, and stop the node runner leaking sockets
`/open_item` carried `allowFullAccessToken`, but it grants an app a permission
on the user's behalf and mints the app token to go with it — a grant and a mint,
which stay session-only. Pre-existing, and missed by the first sweep here, which
only asked whether routes lacking the flag should gain it and never re-read the
ones that already had it. Refused at the top of the handler too, so a credential
that gets past the gate cannot leave an ACL row behind.
The node runner loads each SDK into a vm context and never closed it, so every
test leaked a socket, and every socket holds a per-(user, origin) connection
slot. Admitting the account's own token to the handshake pushed that past the
cap, and the whole events block failed with `events_connection_failed` while the
browser and workerd runners — which do not leak — passed. Closing the FS socket
and the events channel after each test gives the slots back.
* fix: announce only the revokes that can have a socket to drop
Every access-token revoke broadcast an eviction cluster-wide, including the
read-URL tokens `revokeReadUrl` retires constantly. A scoped token is refused at
the handshake, so none of those broadcasts could ever reach a connection.
The announcement now goes out only for a token the handshake would have
admitted: full access, no app in the chain. `revokeOwnAccessToken` refuses
full-access tokens outright, so that whole path is silent; a revoke by raw uuid
says nothing about the token and still announces, which costs a no-op broadcast
rather than leaving a revoked socket up.
Fails without it: the test revokes a scoped token and a full-access one through
the same entry point and counts the announcements.
* test: pin the mint route against the account's own token
`/auth/create-access-token` carries only `requireAuth`, so it never showed up
in the sweep over `requireUserActor` routes — the refusal lives in
`AuthService.createAccessToken`, which turns away any access-token actor
outright. That holds today, and a leaked full-access token minting itself a
sibling that survives revoking the original is exactly what the token swap is
meant to rule out, so it is worth a test rather than a reading.
* fix: let the recommended-apps test follow the list it tests
Two commits on main rewrote `RECOMMENDED_APP_NAMES` without touching the test,
which seeds `editor` and `camera` and asserts the result contains them. Neither
is on the list any more, so the resolved set came back empty and `test (base)`
has failed on every PR since.
The test now seeds the first two names off the list itself and asserts on those,
so the next edit to it cannot strand the test: prepending two unknown apps keeps
all four cases green.
* feat: let an extension waive a plan gate for one route or driver method
A plan gate about to refuse a caller now emits
`subscription.gate.<surface>` through `emitAndWait` first:
`route.<method>.<path>` for a route, `driver.<iface>.<method>` for a driver
method, `share.anyone` for link shares. The payload carries the actor, the
request and the requirement, and a listener that sets `allow` waives the plan
for that call. Callers the plan already covers never reach the hook, and a
listener that throws leaves the refusal in place.
* feat: mint godmode apps their own full-access token
`/auth/get-user-app-token` answers a desktop launch of a godmode app (the
`godmode` column) with a full-access token instead of an app token. Origin
lookups, the sign-in popups for pages outside the desktop, keep getting
ordinary app tokens, and only a web session may mint one.
The token carries the app in `godmode_app_uid`, read into
`accessToken.godmodeApp` for attribution only: `effectiveApp` stays null, so
it keeps the account's reach and passes the same gates as a personal access
token, including the vendor-compatible AI routes. It is refused once the app
is gone or no longer godmode.
Its row is an `access_token` session parented to the web session that asked,
with `app_uid` set, so signing out, revoking that session or revoking all
sessions takes it along, and the sessions list shows it under the app. Each
mint re-signs onto the same row with a 12h `exp` and moves the row's expiry
out, so the desktop keeps an open app alive while a copy taken earlier still
expires on schedule.
A full-access token may now mint scoped tokens (read URLs), parented to its
own row; it still may not mint full access. Revoking a full-access token
cascades to the tokens it minted.
An access token whose session row has a parent now authenticates only while
that parent is live, so a read URL a godmode app minted lapses with the app's
token, and through it with the desktop session.
* feat: launch godmode apps on their own token and renew it
The desktop asks for a godmode app's token at launch and blocks the launch if
it can't get one, as it does for other apps; a godmode app never runs on the
desktop's session token. While the window is open the desktop re-mints the
token once less than 6h of its 12h remain, checking on a timer and when the
tab becomes visible, and answers the app's `reauth_required` with a fresh one,
posted to the app's origin.
puter.js keeps a godmode token in sessionStorage like a session token, never
in localStorage. In app mode a 401 on a godmode token, background requests
included, asks the desktop for a new token and replays, and a `puter.token`
message is only taken from the embedding frame.
The sessions list shows a godmode token row with its app's title and icon.
* fix: keep a godmode token the same across renewals
Apps hand their launch token to code that never sees a replacement (the
terminal's Drive mount, a CLI's API key), so a token re-signed every renewal
broke those copies once the first one expired. The JWT now carries no `exp`
and a renewal moves only its row's expiry, so every copy keeps working while
the desktop keeps the app alive.
What still bounds it:
- only the desktop's web session can extend it; using the token never does,
and signing out or revoking that session revokes it;
- it lapses 12h after the desktop last asked, and a lapsed row is never
revived: the next launch gets a new token;
- however often it is renewed, a token is replaced after 7 days, and the old
one is revoked on the spot along with the tokens it minted and its sockets.
The desktop no longer re-posts a renewal that kept the same token, so the
app's sockets aren't rebuilt every few hours; an app that reports its token
stopped working is always answered.
* fix: reserve the feedback and no-reply mailbox names again
Signup checks names against `util/reservedUsernames`, but `fbl`, `noreply` and
`no-reply` were added to a second copy of the list in AuthController that
nothing reads any more, so they became claimable. Move them into the live list
and drop the copy.
---------
Co-authored-by: Daniel Salazar <daniel.salazar@puter.com>
|
||
|
|
83d64ac685 |
fix: follow-ups from the reviews of #4047, #4049, #4051, #4053, #4055 and #4057 (#4068)
* fix(auth): issue reauth tokens only for browser sessions
A rejected token's 401 carried a signed reauth_token, which /signup turns
into a session on a temp account. Any revoked app or access token, a stale
token from a deleted app whose uid was reused, or a worker credential could
get one. Only a GUI/browser session's rejection now carries a reauth_token;
everything else still gets reauth_required without it. The stale-app check
also runs before the session is touched.
* fix(events): refuse worker creation from an events handler
workers.create minted a worker token with no handler depth, so a handler
could deploy a worker and chain writes through it at depth 0. A handler
actor can no longer create a worker; delivery, deploys and hot reload mint
from plain owner actors and are unaffected.
* fix(ai-ocr): bound the page count's parsing and close its under-counts
- Object-stream headers are capped (/N, /First) and read incrementally, and
the inflate budget scales with the file, so a crafted PDF can't spend
seconds and gigabytes before the credit check.
- An escaped /ObjStm type, an indirect /Count, untyped page leaves and an
object redefined to a smaller tree no longer lower the count.
- An account with no balance is refused before the file is parsed.
* fix(fs): add a switch to stop accepting pre-binding signed URLs
Signed URLs issued before owner binding are still accepted, and owner-signed
ones never expire, so the move-to-another-tree attack stays open for them
indefinitely. legacy_file_signatures (default on) can now turn them off, a
rate-limited warning counts their use so it's visible when they've died
out, and a non-UUID uid is refused before the entry lookup.
* fix(fs): hide the issuer's home uid from a scoped token's entries
A token scoped below the home could still learn the home's uid as the
parent_uid of a direct child (e.g. stat on ~/Documents). For access-token
actors that can't list the home, a direct child's parent uid is now null in
the v2 and legacy entry shapes, using the same rule as the root listing.
* fix(gui): ask for the 2FA code on the last step, right before enabling
The setup flow verified a code before the recovery-codes screen and sent
that same code to enable afterwards; by then it had usually expired, and
the user was sent back to the start. The code is now entered last and goes
straight to enable; a wrong code keeps the user on that step, and other
failures show the server's message. Enter submits the password step.
* test(fs): expect the legacy-signature flag in the signing config
* fix: apply the review's follow-ups to the merged-PR fixes
- Reauth tokens also require the session row to be a browser ('web')
session, not just the absence of the worker claim.
- The OCR page count only treats an untyped object as a page when it sits in
a page tree's /Kids, so outline items, widgets and name-tree leaves can't
inflate an honest PDF's count.
- The signed-URL uid guard checks the uuid's shape only, so a legacy row
whose uuid has nonstandard version/variant bits still resolves.
|
||
|
|
3dfc70cab7 | fix: clarify team directory app access (#4031) | ||
|
|
ebf5086e1c |
fix(fs): keep restore metadata out of reach of anyone but the tree's owner (PUT-2050) (#4063)
Follow-up to #4048. original_path/original_name decide where a GUI restore lands, and the owner's restore is unchecked, so whoever sets them can aim a file at another app's AppData root and block that app's launch. #4048 only pinned them on move for non-account actors; a write could still set them, and a share recipient's session counts as account context. Writes now drop the trash keys for every caller. On move, only the account that owns both source and destination may set them; anyone else moving into the owner's Trash gets them recomputed from the entry, and other moves drop them, including keys already on the row. Copies drop them on every branch. The GUI only offers Restore for direct children of Trash, since nested rows never carry legitimate trash metadata. The AppData/Trash checks for cross-app deletes and create grants now ignore case, like the root guard. |
||
|
|
9ca77be5a9 |
fix(auth): require a step-up to set up 2FA and a valid code to enable it (#4051)
Setup returned a fresh secret and recovery codes to any session, and enable flipped otp_enabled without proof of the secret, so a stolen session could enroll its own authenticator and lock the owner out. Setup now lives at /user-protected/setup-2fa behind the same password / OIDC revalidation gate as disable-2fa, and enable requires a live code for the stored secret. The GUI setup window asks for the password (or opens the revalidation popup) first, and sends the code it already collected to enable. |
||
|
|
31e5b64a0f | fix: make app-data delete grants imply write and read (PUT-1998) (#4022) | ||
|
|
d5e6d5ea07 |
Merge pull request #4007 from HeyPuter/juancastro/put-1896-file-and-sandboxed-iframe-origins-get-a-hard-400-and-cant
fix: refuse opaque origins cleanly instead of a logged 400 (PUT-1896) |
||
|
|
05e2e7428a |
feat(gui): let users remove their profile picture
The account tab only offered changing the avatar. Add a "Remove photo"
action, shown while a picture is set, that clears it via
update_profile({ picture: null }) and resets every avatar to the default.
On failure the hint line says so and the picture stays.
The tab can render before the profile loads, so the profile loader also
reveals the button once a picture arrives. Also move the avatar hint
into i18n.
|
||
|
|
29c6f78b7c |
fix: refuse opaque origins cleanly instead of a logged 400
A page the browser gives no origin to — one opened straight from disk, or an iframe sandboxed without `allow-same-origin` — serialises its origin as `"null"`. `appUidFromOrigin` logged that at error level on every attempt and the GUI sent it on three endpoints, so each refusal cost a 400 and an error-level line while the user got nothing useful. An opaque origin cannot name an app to mint a token for, and is not a valid `postMessage` target to deliver one to, so these clients are refused rather than supported. Make the refusal quiet, early and legible instead: - AuthService: drop the `console.error` and say what the caller should do. The accept/reject set is unchanged; a 400 is already counted per route. - puter.js: `signIn()` rejects with `unsupported_origin` before opening anything, which covers implicit auth too since it routes through `signIn`. The load-time warning now covers sandboxed iframes, console-only — a modal belongs in nobody else's embed. - GUI: a popup whose opener has no attested origin says so and closes, instead of rendering a blank window and wedging on a failed exchange. Also fixes two faults this made reachable: `PuterDialog.open()` prefers the popup branch, which reaches the `puter` global before the constructor that assigns it has returned (`puter is not defined`, no SDK at all); and `showModal()` throws where modals are blocked. `openNotice()` does neither. Ordinary http(s) sign-in is unaffected. |
||
|
|
c542e4675b |
Merge pull request #4000 from HeyPuter/juancastro/put-2036-shared-file-issue
fix: let a share recipient create entries in a folder shared with them |
||
|
|
fc62ea2ee1 |
Revert "variable color for text in input fields (#3996)" (#4001)
This reverts commit
|
||
|
|
ec4d3b31cf |
fix: let a share recipient create entries in a folder shared with them
A recipient addresses the owner's entries by a masked `/<owner>/<uuid>/<name>` path. Routes that resolve an existing row unmask it on the way in, but the ones that create a new entry had nothing to resolve and ran the ACL check against the mask itself, which names no row — so mkdir, touch and the batch write/mkdir/ shortcut ops answered 404 `subject_does_not_exist` inside any shared folder. They now expand client paths the same way every other legacy route does. The GUI events carried the same confusion the other way: the payload was masked for whoever made the request but addressed to the entry's owner, who cannot resolve another user's mask. The owner's client dropped the row by uid and then failed to re-add it under a path it had never heard of, so anything a recipient touched vanished from the owner's open window until a refresh. Those events now publish the owner's real path; responses to the actor stay masked. Also fixes the desktop's `item.moved` handler reading `metadata` as an object when the wire carries a JSON string: the name of a trashed item came out empty (trashing renames the entry to its uid), and re-encoding the string left the restore path parsing a string instead of an object. The parse the dashboard and the explorer already open-coded is now one helper. |
||
|
|
8862420594 | variable color for text in input fields (#3996) |