Add `requireVerified: true` to both app feedback routes (`GET /target` and `POST /`) in `AppFeedbackController`. This tightens access control so only verified user accounts can fetch feedback targets or submit app feedback.
The Files tab's folder-background context menu (empty listing area,
sidebar folders, breadcrumb segments) now offers Publish as website and
Properties for the folder the menu was opened on, matching the desktop's
folder menus. Both are omitted at the filesystem root and in Trash, and
Publish is omitted inside trashed folders.
The properties modal can now resolve an entry by path when the caller
has no uid, which the folder-background menu doesn't.
The publish gate (account creation + email confirmation) was duplicated
in three places, so it moves to a publish_as_website helper that all
call sites now share.
Changes are:
- global egress metering
- remove file egress cost
- introduce file op cost for the per request cost s3 has
- enforce fs read/download etc to through 402 when out of usage; allow for subdomains
- enforce kv metering when out of usage through 402; allow for workers
- jsdoc as source of truth for puter.js types
- kv driver caching for get and batchget operations with decreased costs
Both throw sites are in one call and only one can ever run, so a plain
const reads the same and drops a function that existed only to defer a
constructor.
The service owns every feedback business rule — target resolution,
eligibility, message normalization, the durable caps, and the owner-email
preconditions — but was only reachable through the controller's tests. Give
it and the store their own suites so a regression names the layer it broke.
Service coverage adds the branches the route tests could not reach: a
blocked origin resolving to null rather than surfacing a 403, owners who are
suspended or unsubscribed, length measured after normalization, the 24h cap
window boundary, subject-header injection via the app title, and the email
links being rooted at config.origin.
The two describes already labelled `AppFeedbackService ...` move out of the
controller test, which keeps only the caller-facing promise that a failed
send still returns success.
Translate the app discovery, app request, desktop shortcut, and app folder controls that currently fall back to English in the Traditional Chinese interface. Preserve the positional placeholders used by accessibility labels.
1c2d33f14 added :not(:disabled) to the base .app-feedback-btn hover
rule, which raised its specificity above the primary button's hover rule
— so hovering Send swapped the blue gradient for the light hover
background, leaving white text on a pale pill. Give the primary hover
rule the same :not(:disabled) so it wins by source order again.
The footer told developers to turn feedback off via a puter.apps.update
one-liner, but the toggle lives in the Dev Center app settings — and
Dev Center is where apps get feedback enabled by default in the first
place. Reword it to 'manage it in the Dev Center' with a link built
from config.origin (like app_link) so it holds on self-hosted
deployments.
Under COOP the popup's opener link is severed, so the SDK deliberately
resolves false while the popup stays open and the user can still submit
(a feedback submission has no server read-back the way a permission
grant does). The documented contract ('resolves to true if the user
submitted feedback') was silently wrong on cross-origin-isolated pages —
state the limitation in the doc and the SDK jsdoc: false means 'not
confirmed', not 'not sent'.
The doc claimed the dialog 'tells the user their username will be
shared', while the dialog's note talked only about the email address and
the implementation shares the username always plus the email (as
Reply-To) only when verified. Describe the actual disclosure: username
always, email when verified.
Subjects were compiled with default Handlebars escaping, so the
app-user-feedback subject rendered a title like "Bob's App & Games" as
"Bob's App & Games" — literal entities in the recipient's mail
client. Subjects are plain-text headers, not HTML; compile them with
noEscape. Header safety is unaffected: the transport encodes newlines
and free-form values collapse whitespace upstream.
The note unconditionally said 'Your email address will be shared with
the developer so they can respond', but AppFeedbackService shares the
username always and the email only when it exists and is verified — an
unverified or temp-user sender was promised a reply path that never
materializes, and nobody was told about the username.
Show 'username and email' when the signed-in user's email is verified,
and 'username' otherwise.
readTargetParam accepted values up to 3000 chars but the raw origin is
stored verbatim into source_origin VARCHAR(2048) (MySQL/Postgres), so a
2049-3000 char origin passed every validation and then blew up the
INSERT with an HTTP 500 on Postgres/strict MySQL — or was silently
truncated on non-strict MySQL, corrupting the abuse-forensics value the
column exists for. Cap the param at the column size.
Escape and backdrop clicks were already ignored while the POST was
pending, but the X, Close, and Cancel buttons weren't — clicking one
mid-send settled the promise false and tore down the overlay while the
submission still landed server-side: the developer got the email, the
app was told sent=false, and a user who resubmitted 'the failed one'
sent a duplicate and burned a daily-cap slot.
Apply the same !sending gate to the buttons and disable them visually
while the send is in flight.
launch_app uses options.app_obj verbatim when provided, and the
suggested-apps launch paths (open_item.js, UIWindowSearch.js,
UIDesktop.js) pass summaries from toAppSummary — which omitted
feedback_enabled. So an opted-in editor launched by opening a .txt file
showed no Send Feedback control in the dashboard drawer, while the same
app launched from the Apps tab (full puter.apps.get object) did.
The showFeedbackDialog IPC handler had no re-entry or abuse guard, and
the dialog it opens is a full-viewport overlay above the taskbar and
every window — so 'while (true) await puter.ui.showFeedbackDialog()'
kept the desktop permanently covered (for signed-out users, the same
loop spams the full-page signup window instead). Every dismissal just
settled the promise and let the app immediately reopen it.
Allow one dialog at a time, and back off reopens per app after each
dismissal that sent nothing: 10s, then 60s, then blocked until page
reload. A successful send resets the backoff, and user-initiated paths
(dashboard drawer) are unaffected since they don't go through IPC.
In the app environment showFeedbackDialog awaited an IPC reply with no
capability check. A host GUI that predates this feature (self-hosted
Puter running the live js.puter.com SDK) has no handler for the message
and never replies, so the promise documented as 'never rejects' also
never resolved.
The GUI now advertises the IPC dialogs it can answer via a
puter.gui_features param on the app iframe URL, and the SDK resolves
false when 'feedback-dialog' isn't listed. A reply timeout could not
substitute: legitimate replies only arrive when the user closes the
dialog, so any timeout would false-negative while the user is typing.
Older SDKs ignore the extra param.
With no email transport configured (the common self-hosted default),
submissions were stored in app_feedback — a table with no read path
beyond the abuse-cap COUNTs — the owner email was silently skipped, and
the sender was still shown 'Feedback sent. Thank you!'. The developer
never learns the feedback exists while the user believes it was
delivered.
Gate acceptsFeedback on clients.email.isConfigured so the pre-flight
reports enabled:false (the dialog shows its 'not accepting feedback'
pane) and submit returns 403 instead of swallowing messages. Owner-level
store-without-email cases (unconfirmed owner email, per-app email cap
overflow) keep their existing deliberate semantics.
Every embedded_in_popup boot ran the user-app token exchange, and the
exchange is a write: /auth/get-user-app-token bootstraps an app row for
the opener origin, grants flag:app-is-authenticated (what makes the
site count as connected to the account), and creates its AppData dir.
So merely opening — or immediately cancelling — a send-feedback popup
recorded a user<->site relationship the read-only feedback flow never
needs: the server resolves the feedback target from the attested origin
without any of it.
Gate the exchange behind runsUserAppTokenExchange(action) in all three
popup paths that mint (main postAuthActions exchange, temp-user signup
success, manual signup fallback). request-permission keeps the exchange
since grants are written against the app row it bootstraps.
The Dev Center deliberately creates apps with feedbackEnabled (see
32c950de5), but the SDK docs, apps.d.ts, and AppFeedbackService's class
doc all described feedback as strictly opt-in / default-false with no
qualification — so a Dev Center developer reading them would wrongly
conclude feedback is off for their app. State the Dev Center behavior
alongside the API default, and correct the update-path docs: an omitted
feedbackEnabled leaves the current value unchanged rather than
defaulting to false.
The per-user and per-user-per-app caps were check-then-insert and the
per-app email cap was count-then-send, so parallel requests (or multiple
nodes, or the route limiter failing open) could all read a stale
under-cap count and push past every limit — the exact scenario the
DB-backed caps exist to stop.
Now the user caps recount after the insert (own row included) and roll
the row back with 429 if a burst breached them, and the email cap claims
its slot (email_sent=1) before sending, recounts, and releases the slot
if over cap or if the send fails.
APP_NAME_REGEX allows names beginning with "app-" (e.g. the seeded
app-center), but resolveTargetApp's startsWith('app-') heuristic sent
those to a uid-only lookup with no name fallback, so feedback for such
apps 403'd even when enabled. Use AppStore.resolveApp (uid, then name)
like the rest of the codebase.
Address four issues with the app feedback UI:
- Dashboard app-drawer: the extra "feedback" control pushed the close
button past the drawer's derived width and clipped it. A `has-feedback`
modifier widens the surface by one button + gap so all three controls
fit. The control's glyph is now a message bubble with text lines, which
reads more clearly at 14px than the previous bare speech bubble.
- The feedback dialog is no longer a UIWindow. It's a from-scratch
overlay modal in the spirit of the dashboard modals (uninstall,
add-app): a fixed scrim + centered card with self-contained,
theme-aware color tokens (light default + dark override), a bottom-sheet
layout on narrow screens, backdrop/Escape close, and an entrance
transition. This renders consistently across the three contexts it's
opened from (desktop app-IPC, dashboard drawer, standalone popup), so
the callers no longer pass UIWindow-specific window_options.
- Feedback now shares the sender's email (not just their username) with
the developer so they can respond: the owner email sets Reply-To to the
sender and shows the address in the body — but only when the sender's
email is verified (an unverified address could be anyone's, so it's
never used as a reply target). EmailClient.send gains an optional
replyTo. The dialog note now says the email will be shared.
Tests: e2e updated for the new modal (7 pass); backend feedback suite
covers the verified/unverified sender-email split (sqlite + postgres);
EmailClient + GUI unit suites pass; type-check clean.
New apps created in Dev Center now have feedbackEnabled set on creation,
so users can send the developer feedback without any extra setup. A "User
Feedback" toggle in the app's settings lets developers turn it off (and
back on); it's wired into the save payload, the dirty-state tracking, and
the reset-to-original path like the neighboring toggles.
The Save update omits feedbackEnabled unless the toggle is present, and
the backend leaves an omitted field untouched, so the default survives
the create-then-save flow Dev Center runs. Add an SDK apps-suite guard
covering that round-trip (create-on -> unrelated update keeps it -> can
be turned off).
Surface the feedback dialog directly from the app window's chrome in
dashboard mode: apps that opt in (apps.feedbackEnabled) get a "Send
Feedback" button in the dashboard app-drawer, next to minimize/close.
It opens the same UIWindowAppFeedback dialog, targeting this app by uid.
The control is only rendered when the app opted in — feedback_enabled is
threaded from the launched app's metadata into the window options — and
the dialog still re-checks opt-in server-side, so a stale flag can't send
anywhere. Reuses the existing .dashboard-app-drawer-btn styling and the
app_feedback_title i18n string, so no new CSS or strings.
Adds e2e coverage: the control appears and opens the dialog for an
opted-in app, and is absent for an app that hasn't opted in.
Add puter.ui.showFeedbackDialog(), letting users send feedback to an
app's developer. In the app environment the Puter desktop renders the
dialog; on a third-party website a puter.com popup hosts it. The message
is stored in a new app_feedback table and emailed to the app owner's
confirmed email — it never passes through the app's own code.
Feedback is strictly opt-in per app via a new apps.feedback_enabled
column (a real column, not an app-metadata key, so Dev Center's
whole-blob metadata saves can't silently erase it), settable through the
existing puter.apps.update path (feedbackEnabled).
Backend follows the layered stack: AppFeedbackStore (durable count
queries) -> AppFeedbackService (opt-in check, message normalization,
abuse caps, best-effort owner email) -> AppFeedbackController
(POST /app-feedback, GET /app-feedback/target). New app-user-feedback
email template uses the escaping-safe nl2br triple-stash.
Defensive by design:
- requireUserActor blocks app tokens, so feedback can't be submitted
programmatically; guiOriginOnly keeps cross-origin pages out.
- App identity comes only from the validated IPC sender (desktop) or the
browser-attested opener origin (popup), never from message contents.
- The send-feedback popup action is in NON_AUTH_POPUP_ACTIONS, so it
never delivers a token to the opener.
- Layered limits: route rate limits, plus DB-count caps that fail closed
when the limiter backend is down, plus a per-app daily owner-email cap.
- Owner email is fully best-effort: an unconfigured transport,
unconfirmed/unsubscribed/suspended owner, or send failure never fails
the request or blocks storage.
- The dialog and SDK method are resolve-only and always settle, so a
caller is never left hanging.
Migrations for sqlite/mysql/postgres, puter.js types, docs, backend
tests (sqlite + postgres), and a Playwright e2e spec are included.
- declare rate + concurrency limits on every route and driver that lacked one
- add acquireConcurrent for websocket connections and the DAV mount
- bucket AI models by identity key only; keep resold duplicates of any vendor
- skip recently-failed provider routes; cap the fallback chain at 3 attempts
- let full-access access tokens bind a worker to an app their own user owns
- cache resolved subscriptions so tiered limits don't add a round trip
The rows already answer hover with a --select-ring border; filling them
with the grey --dashboard-hover under that accent ring read as two
systems disagreeing. The weak select tint keeps the hover in one voice
and stays under the icon chip's stronger tint in both themes.
Update the English `add_app_ai_desc` translation string in the GUI to a more direct, enthusiastic message: “Describe the app you want and AI will build it!”.
The godmode gate in ExecService.launchApp checked `godmode === 1`, but
GET /apps/:name serializes the flag as a boolean, so the check always
failed and file_paths launches (e.g. Dev Center opening a worker's
source in the editor, #2218) silently dropped the file. Accept both
shapes, matching the existing guard in launch_app.js.
Also fix the two issues hiding behind that gate:
- puter.apps.get returns `uid`, not `uuid`, so the /sign call for the
target app sent app_uid undefined — no write grant and no user-app
token for the launched app. Use `uid` with a `uuid` fallback.
- The closeApp IPC handler had the same `godmode === 1` comparison,
preventing godmode apps from closing other apps' windows.
60686643b made App Center and the AI builder morph out of the + tile the
user clicked, which answers where the window came from but not the
question these two options exist to answer: where the APP went. The grid
never showed it arriving, so the user still ended a launch with nothing
to find, no tile for minimize to fly back to, and no lesson learned.
Choosing either option now plays exactly what an /app/<name> landing
plays (beginDeepLinkLaunch): when the app already has a tile the intro
travels to it, plays the click flourish, and the window grows out of the
app's own slot; when it doesn't, the tile is INSTALLED first — the slot
opens, the progress stroke draws, the icon springs in — and only then
does the window morph out of the tile that just arrived. Same beats,
same exposure decay, same interruptibility, same duplicate-launch claim.
The app info is prefetched in parallel and handed to launch_app, the
same contract as initgui's landing path, so the intro (which may need to
draw the arriving tile from it) never costs a second round-trip.
This also reverts 60686643b's anchor plumbing (UIWindow's
dashboard_tile_el option and the dashboard_tile_in_view export): the
intro installs a real tile for the by-app-name morph to find, so no
stand-in anchor is needed.
Choosing App Center or the AI builder opened the window with the plain
fade while every other launch in the Apps tab grows out of the icon that
was clicked — the one launch that most needs to say where it came from
read as the app appearing from nowhere.
The morph resolved its anchor by app name, which cannot work here: the
whole point of these two options is installing an app you do not have,
and an app with no tile has no name to find. So the launcher may name its
anchor element (dashboard_tile_el), and TabApps resolves it once before
launching so the click flourish and the window's half can never disagree
about which tile the launch came from: the app's own tile when it already
has one on screen — minimize flies the window back there, so that is
where opening should come from — and otherwise the + tile itself.
dashboard_tile_in_view's visibility test is now a named helper it shares
with the new path, so an anchor on an offscreen pager page is still
rejected and still falls through to the fade.
The Apps grid showed what you have and offered no way to get more. A
dashed plus tile now rides at the tail of every page of it: after every
app, on the last page, and nowhere else. It wears the same tile skeleton
as an app so hover, focus, and arrow keys treat it identically, but
carries no app name or group id — which is what keeps it out of the
saved order, the running dots, and the deep-link tile lookups. It cannot
be picked up, is never a drop target (so no app can land past it), has
no context menu, and sits still while the apps jiggle in reorder mode.
Search results leave it out: a query asks which apps you have.
Clicking it asks how you would like to get an app: find one in the App
Center, have one built by the AI builder, or request one that does not
exist yet. The request is a form in the same card rather than the stock
Contact Us window — a titled pane one step deeper, whose Back arrow
returns to the options with the draft intact. It posts the same
/contactUs request that window did; a failure is said inside the form,
where the request is still there to retry, rather than in an alert
window the next click on the dashboard would bury.
Extract app tile URL generation into a shared helper and use it for a new dashboard context menu action that copies the tile's destination. This keeps copied links aligned with the existing open-in-new-tab behavior and adds regression coverage for app, external, encoded, and folder tiles.
A /app/<name> landing for an app the dashboard didn't list opened with
a plain fade and left the app NOWHERE: the grid had been fetched before
the launch granted the permission that installedApps reports, so Back
found no tile to minimize into and the user landed on a dashboard
without the app they were just inside. The round trip that teaches
"windows are inflated tiles" broke on exactly the landing where a new
user is forming that model.
Now the landing SAYS what it does. The app joins the grid at the tail
(where new apps land) with a tile drawn from the landing's own app-info
prefetch -- only a confirmed-real app ever materializes, with its real
icon -- held invisible until the intro has revealed the grid and
travelled to the tile's page, and then it is INSTALLED, in the grammar
every app store taught: the slot opens (the folder well's own surface)
with the icon dim inside it, a progress stroke draws clockwise around
the slot, and on completion the icon springs to full color and size as
the label names it. Only then does the flourish play and the window
grow out of the tile. The arrival is the one beat that never decays:
per-app news delivered at most once per app, not a repeated lesson.
Minimize now has a target, and the app is simply there afterward.
The regression surface is guarded on every edge: the parked-invisible
state is re-applied by renderApps itself so mid-intro re-renders can't
strip it, and settleDeepLinkLaunch -- which the landing's finally
always reaches -- force-reveals it, so no path leaves an invisible
tile. A _pendingInstalls overlay (the mirror of _removedLocal) keeps a
refresh fetched before the grant lands from evicting the tile, and
retires itself once installedApps confirms the app or the user
uninstalls it again. The order is never saved on the splice, so the
default-position append can't freeze a custom order the user never
made. Failed or absent app info adds nothing; installed-app landings
are untouched; animations-off and reduced-motion users skip the
choreography but still get the tile, silently. The progress ring is a
transient element (the tile's ::before is the well, its ::after the
running dot) drawn as an SVG dashoffset sweep -- no Houdini dependency
-- and the app-info prefetch now asks for the 128px icons every other
tile already uses.
The control drawer on headless dashboard apps opened itself on EVERY
window open, forever -- 2.6 seconds of tray over the app's own top
edge to teach a lesson (the controls live in this tongue) that is
learned in a few exposures. Same doctrine as the dashboard's
deep-link intro: pedagogy that knows when to step aside.
A per-account kv counter records deliveries, once per window: a flash
played while the user could see it, or the user opening the drawer
themselves (hover, tap, focus) -- the stronger proof, since the
drawer is a headless app's only chrome and must never decay out of a
user's awareness. After three, new windows keep the bare tongue and
the flash stops; hover, tap, and focus expansion never decay.
The attach is synchronous and the stored count is not: the first
drawer of a session races the read against a short cap and defaults
to teaching -- a hung read must not hold the intro hostage, and every
failure mode (unparseable value, failed read, timed-out read, failed
write) errs toward teaching once more, never toward never teaching.
The resolved count is cached and kept current in memory, so later
windows decide synchronously and a session's own not-yet-written
increments still count. kv.incr keeps two devices from losing an
increment, and the pre-read caps the key once the lesson is learned.
If the user beats a still-deciding flash to the drawer, the flash is
skipped rather than played over their hand -- its auto-collapse would
shut a drawer they are actively using. A flash in a hidden tab still
defers to first reveal (it rides requestAnimationFrame, as before)
and an unseen one is never counted.
* feat(perms): add cross-app app-data permission vocabulary
* feat(perms): sweep app grants by permission prefix
* feat(perms): resolve and withdraw cross-app data grants
* feat(kv): support an authorized namespace override and per-key privacy
* feat(kv): gate cross-app KV access behind app-data grants
* feat(fs): allow cross-app AppData access and require a scope to delete
* feat(auth): accept permission lists and gate app-data grants
* feat(perms): add requestAppData to the puter.js SDK
* feat(gui): carry permission lists through the IPC and popup transports
* feat(gui): describe cross-app data requests in the consent dialog
* docs: document requestAppData and per-entry KV privacy
* perf(perms): sweep cross-app grants only for origin-bootstrapped apps
* fix(gui): stop double-encoding cross-app consent text
* fix(perms): close three gaps in cross-app grant enforcement
* fix(kv): meter and batch the per-entry privacy probe
* fix(perms): resolve app identifiers and scopes more strictly in the SDK
* test(perms): cover the cross-app consent flow end to end
* fix: small missing token resolution for app
also adds the same exclusion for the batchPut api, small change
* fix: make resolved actor optional
---------
Co-authored-by: Daniel Salazar <[email protected]>
Three fixes to the My Apps folders:
A folder big enough to need it now paginates like the grid outside
(iOS folders page too) instead of growing a scrollbar: pages of
cols x 3 tiles in a scroll-snap scroller with dots below, wheel and
swipe to flip, and a drag held at the card's edge flips folder pages
so reordering works across them. The card's height stays constant
across pages, and a deep-link landing on an app that lives on a later
folder page travels there before the launch morph.
Dragging an app out of the open folder no longer drops it blind:
after a beat held outside the card, the app leaves the folder, the
card closes behind it, and the SAME drag carries on over the grid --
placeholder shuffle, edge page flips, even a drop into another
folder -- so the app lands where the user watched themselves put it.
A quick drop during the beat still lands beside the folder, and
swinging back inside within the beat cancels cleanly.
The folder well's fill played twice on nearly every merge: the rest
countdown anchored at first contact with the target, so the hand
decelerating INTO the tile always read as movement when the dwell
elapsed, and the deliberate refill-from-empty replayed the fill. The
countdown now re-anchors on the move events themselves -- rest is
only visible at its edges -- so the fill that completes is the one
started by the last movement, played exactly once.
* feat(gui): iOS-style app folders in the My Apps grid
Drag one app onto another and let it settle: a well opens under the
target and the drop makes a folder of the two. Dropping onto an existing
folder joins it. A folder opens by growing out of its own icon into a
card over a blurred grid, where its apps can be launched, rearranged,
renamed, or carried back out.
Hovering a tile mid-drag means two things — "push over, I'm passing
through" and "swallow me" — and the tile is barely bigger than its icon,
so pixels can't separate them; motion does. The shuffle is held while
the folder offer stands and fires when the icon leaves the tile, or at
the drop, so a quick drop onto a neighbour still reorders exactly as it
did. The offer itself re-arms rather than cancelling on movement: the
last events of a drag are the ones carrying the icon onto the target and
nothing is dispatched while it rests, so a cancel-on-movement dwell
could never fire at all.
Folders are stored in their own kv key; the grid's ORDER stays entirely
in the existing saved app order, with a folder occupying its first
member's slot and its members contiguous. Every saved order therefore
stays valid with no migration, and an app whose installedApps page
failed to load keeps both its folder and its position. Folders never
nest, one that drops below two apps dissolves, and a corrupt kv value
degrades to "no folders" rather than a broken tab.
Elsewhere:
- Search looks THROUGH folders — a match the user then has to hunt for
inside one is not an answer.
- Minimize morphs into the FOLDER when an app lives in one, and a
/app/<name> landing opens the folder so the launch grows out of the
icon where the app actually is.
- The uninstall FLIP keyed surviving tiles by app name, which a folder
tile doesn't have; it now keys by identity.
New pure model in appGroups.js with tests; verified end to end in the
running dashboard (create, join, open, rename, reorder, eject, ungroup,
launch-from-folder) alongside plain reorder, search, and uninstall.
* fix(gui): keep the folder name field from inheriting input[type=text] sizing
style.css styles every input[type=text] with `width: 100%` and grows it to
`padding: 7px; border: 2px` on focus. `input.myapps-group-name` matches at the
same specificity, so it only wins the properties it actually declares — width
was never one of them, and the focus rule declared neither padding nor border
width. The name field therefore spanned the entire folder card (so its hover
and focus chip read as a full-width bar rather than the name) and grew 8px
taller the moment it was clicked, shoving the folder's app grid down.
Spell the three out, in both the resting and the focus rule — the same trap
.myapps-search already documents next door.
* fix(gui): size folder icon ghosts to the icon they stand on
Border-box only reaches a folder's icon through `.dashboard * { box-sizing }`,
and every ghost cloned from one is appended to <body>, outside that rule: the
drag ghost, the click-time launch flourish, and the open/minimize morph ghosts
all fall back to content-box, where .myapps-group-icon's 5px padding is added
to the 56px slot. Each ghost rendered 66px square and 5px off, so it visibly
popped at exactly the moment it was supposed to sit flush on the real icon.
State box-sizing on the rule itself so a clone carries it wherever it lands.
* fix(gui): stop a closing folder from swallowing the next click
_closeGroup drops the open class and leaves the overlay in place for
GROUP_PANEL_CLOSE_MS so the card can recede into its tile. The scrim is
`position: fixed; inset: 0` and still hit-testable for that whole quarter
second, so a click on the grid during it landed on the outgoing overlay — whose
handler only re-runs _closeGroup, now a no-op. Shutting a folder and reaching
straight for an app did nothing.
Take the outgoing overlay out of hit-testing; it has no interactive job left.
* fix(gui): keep a folder name typed right up to the moment it closes
The name box commits on blur, and every exit that goes through a pointer blurs
it while the folder is still open — so clicking outside, or launching an app
from inside, keeps what was typed. Escape does not: _closeGroup clears
_openGroupId first and only then moves focus to the tile below (or removes the
card outright), so the blur arrives with no open folder to rename and
_renameGroup drops it. A brand-new folder opens with its name selected for
exactly this edit, so "type Games, press Escape" — the obvious way to dismiss
a dialog — was the path most likely to lose it.
Commit the pending name on the way out, before the folder id is gone.
* fix(gui): close an open folder when the Apps tab is re-entered
The dashboard hides an inactive section and calls onActivate on the way back
in; there is no deactivate hook, so a folder left open survives the round trip
and greets the user still open over a grid they walked away from. Worse,
onActivate's focusSearch then lands the caret in the search box behind the
folder's scrim, and typing filters the grid the card is covering — a modal with
the keyboard pointed outside it.
Shut the folder as the tab comes back: returning to the tab is returning to
the grid.
* fix(gui): move focus into a folder when it opens
Opening a folder called .focus({ preventScroll: true }) on a jQuery
object. jQuery's .focus() shorthand reads a lone non-function argument
as event DATA and binds a handler with it, so it never moved focus:
the folder opened modal over the grid with focus still on the tile
behind its scrim, where Tab walked away through the inert grid instead
of cycling inside the dialog — and the object it bound as a handler
threw a TypeError on that tile's every subsequent focus.
Focus the DOM node instead, as every other focus call in this file
already does.
* fix(gui): stop renaming a folder from swallowing the click that commits it
The folder's name box commits on blur, and blur fires on the PRESS —
before the click that press belongs to. Committing re-rendered, and the
re-render replaced every tile in the open folder, so by the time the
click was dispatched the tile under the pointer was detached and the
delegated handler never saw it. Typing a name and then tapping an app
in the folder — the path a brand-new folder puts the user on — renamed
the folder and did nothing else; the app only opened on a second click.
Rebuild the folder's contents only when they actually differ from what
is on screen. A rename doesn't change them, so nothing is detached, and
a background refresh no longer throws away hover/focus either. Tiles
that survive get their drag resting-rects cleared, since the card can
have moved under them since the rects were taken.
* fix(gui): keep Enter in a folder's name box from leaving the folder
Committing the name with Enter blurred the box, which left focus on
<body> — outside a dialog that is marked aria-modal and that traps Tab
on its own subtree. The next Tab therefore walked off through the inert
grid the folder is covering, exactly what the trap exists to prevent.
Step out onto the folder's first app instead; the same blur still
commits the name. The keystroke is stopped at the box because the
grid's document-level key handler reads Enter on a focused tile as
"launch it", and would otherwise have taken the focus move as its cue
to open an app the user never asked for.
* fix(gui): stop an open folder clipping its own uninstall badges
The folder's grid scrolls, so it clips anything outside its padding box
— and reorder mode's uninstall badge deliberately overhangs the top-left
corner of every tile. The top row's badges therefore rendered as flat
tabs rather than circles, on the one surface where they are a touch
user's only way to uninstall an app they have filed away.
Give the scroller 9px of top padding for the overhang to sit in and take
it straight back off as margin, so the card and everything in it stays
exactly where it was.
* fix(gui): hold page edge-flips while a folder merge is being offered
A tile in the pager's last column sits inside the 60px edge-flip zone, so
resting a dragged icon on it — the folder-making gesture — armed the edge
dwell alongside the merge dwell, and the page flipped out from under the
very folder the user was watching form (the merge target scrolls away
mid-offer, and the drop then resolves against its stale resting rect).
Worst on phones, where 72px tiles overlap the zone across the whole last
column.
A live merge offer now holds the edge flip: entering the zone arms no
dwell while an offer stands, and an offer that arrives during a running
dwell is re-checked at the flip (a resting pointer fires no event that
could clear the timer). Carrying the icon off the tile withdraws the offer
and the hold with it, so deliberate flips — resting in the bare edge
gutter — behave as before.
* fix(gui): make Escape cancel a folder rename instead of saving it
Escape pressed mid-edit fell through to the folder's close handler, and
closing commits whatever the name box holds — so the one key every inline
rename uses for "never mind" stored the abandoned half-typed name. Escape
in the box now puts the stored name back and steps out to the folder's
tiles, exactly the cancel Finder/Explorer taught; with nothing left to
cancel (name untouched) it falls through and closes the folder as before,
so a second press still exits.
* fix(gui): refill the folder well when the merge countdown restarts
The merge dwell pins its stillness anchor at first contact with the tile,
and a hand decelerating INTO a target routinely covers more than the
7px allowance between that moment and the first tick — so the countdown
quietly restarts. The well's fill, tuned to the same 460ms, had already
completed by then and just sat there half-open: a drop it seemed to
promise a folder for actually reordered. Restart the fill with the
countdown, so what the well shows is always the countdown that is
actually running.
* fix(gui): keep keyboard focus inside an open folder across its edits
The folder card is a modal dialog, but three of its flows stranded focus
on <body>, where Tab walks the inert grid behind the scrim:
- Remove from Folder rebuilds the card's grid after the context menu has
dropped focus, so nothing inside the dialog holds it. The rebuild now
hands focus back to the same app's tile (or the first) whenever it finds
focus on <body> — never stealing from an uninstall modal, which holds it
legitimately.
- The same eject dissolving the folder re-renders the grid right after
_closeGroup's focus hand-back, replacing the very tile it chose. The
ejected app's own tile — where the user is looking — takes focus instead.
- A click on the card's empty space focused nothing at all. The card now
carries tabindex=-1 so such clicks land on it (no visible ring; a focus
ring around the whole card would misread plumbing as selection), and the
Tab trap wraps from there instead of stepping off through the scrim.
* fix(gui): size the folder name box to the name it holds
The box sat at the browser's default ~20ch regardless of content: a short
name floated in a hover pill far wider than the word, and a 40-character
one clipped while the card had room to spare. field-sizing: content hugs
the text between a 90px floor and the card's width, iOS-style; engines
without the property keep the default box exactly as before.
Signing up from a direct /app/<name> landing redirected to '/' on
success, silently dropping the app the URL asked for — the deep link's
launch (and its dashboard intro) never happened. Signup now mirrors
login's pathname-preserving redirect, but only for app-landing routes
(/app/<name>, /desktop/app/<name>): every other route keeps the
historical '/', notably /action/signup, where returning to the same
path would just show the signup form again. The query string stays
dropped, matching login's credential-leak hygiene. This covers all
three signup entry points reachable from a landing: the login cover's
"Sign up", the session picker's "Create Account", and the
must_login_or_signup fallback.
The session picker (UIWindowSessionList) also left itself on top of
the cover windows its two links open, hiding their username fields:
"Create Account" tried to close the picker via the LOGIN window's
c2a selector (which matches nothing in the picker), and "Log Into
Another Account" never closed it at all. Both now close the picker —
in the reload flows only: the no-reload (popup) flows keep it open,
where it doubles as the fallback UI when the login/signup window is
abandoned mid-flow.
Picking an account was already correct (location.reload() keeps the
landing URL); with these fixes all three picker paths, plain login,
and signup all return to the app landing, where the boot replays the
launch and its intro.
The intro exists to teach the dashboard's spatial model; once learned it
would only tax every bookmarked landing. Two mechanisms remove that tax:
- Interruptible: any real user input (pointer/key/wheel, isTrusted only)
during the intro skips the remaining choreography and launches at
once — it wakes the in-flight beat sleep, so the skip is immediate.
Input never cancels the launch itself and is never swallowed;
whatever it was doing (typing a search, clicking a tile) still
happens.
- Exposure decay: after 3 delivered — or deliberately skipped — intros
the beats collapse and the sequence plays in one breath, exactly like
a warm tile click. Counted per account in kv (kv.incr, atomic across
devices; capped at the threshold so the key stops changing) rather
than per device: the lesson lives in the user's head and the account
follows them, while localStorage would also bleed between accounts on
a shared browser. Only real exposures count: a delivered flourish or
an active skip with the grid on screen — hidden tabs, timeouts, and
no-tile landings teach nothing and don't count. The animated page
flip is exempt from decay: it isn't a repeated lesson, it's live
wayfinding to where this app lives, and its settle is needed anyway
to put the tile in view for the morph. Every failure mode (slow or
failed kv read) errs toward teaching once more, never toward never
teaching.
Also bounds the intro's wait on the app-list load by the same deadline
as the tile wait — fetch has no timeout of its own, so a stalled
installedApps request used to hold the deep-link launch hostage
indefinitely.
The deep-link intro used to flip to the tile's page instantly behind the
grid's load-fade, so landings on off-page apps woke up on page N with
nothing but the pager dots hinting a move happened. Now the grid always
reveals on its first page, holds the grid beat, and then visibly travels
to the tile's page before the tile's flourish — the journey shows where
the app lives, which is also where minimize will put it back.
Smooth scrollTo has no reliable completion event across engines, so the
travel uses a settle allowance (DEEP_LINK_INTRO_FLIP_SETTLE_MS, same
pattern as the drag code's DRAG_FLIP_SETTLE_MS): the scroll's ~450ms
plus a rest so the landing reads before the tile pops. First-page apps
skip the flip entirely and are unaffected; the hidden-page bail-out is
re-checked after the flip settles.
A direct landing on /app/<name> now plays the same sequence a real
Apps-tab tile click does — grid appears, a beat, the tile's icon ghost
pops out of its slot, a beat, and the window morphs out of the tile —
so the landing shows the user what is being opened and where minimize
puts it back.
TabApps.beginDeepLinkLaunch waits (3s cap) for the tile to be genuinely
visible (list loaded, render done, pager flipped to the tile's page,
load-fade revealed, icon painted), paces the beats, and claims the app
in _launchingApps so a click mid-intro can't spawn a duplicate. The
launch's app-info fetch is prefetched in parallel so the intro never
delays the app's own round-trip. No tile, animations off, reduced
motion, or a background tab (hidden pages throttle timers and defer
rendering) all skip straight to the immediate plain-fade launch.
Also fixes the tab title sticking as the app's name after closing a
deep-linked app: the landing's replaceState('/') committed the
dashboard's history entry while the page still carried the server's
app-name title, and Chrome shows an entry's stored title when close/
Back traverses onto it — document.title is now reset before the
replaceState so the entry is stamped with the dashboard's own title.
Right-clicking a sidebar/breadcrumb folder in the dashboard Files tab and
choosing New > Folder (or any file type) created the item in the right-
clicked folder correctly, but the UI inserted the new row and started the
inline rename in whatever directory was currently open — so the item
appeared to be created in the wrong place until a refresh.
Now, when the creation target isn't the directory on screen, navigate to
the target and run the select + rename flow there; same-directory
creation keeps the incremental insert.
Uninstall only revokes permissions, but the recommended launch list is a
global hardcoded set that knows nothing about per-user revokes — so an
uninstalled recommended app's tile came back on every reload. Persist
uninstalled app names in kv (dashboard_removed_apps) and filter only the
recommended merge against them; installedApps is never filtered, so a
genuinely (re)installed app always shows.
The drawer resolved its own icon URL (a differently-sized variant under a
different URL than the dashboard tiles use), so its <img> fetched cold and
the icon popped in mid-intro. Reuse the bitmap an Apps-tab tile or Home-tab
recent already decoded — instant from the image cache; when nothing is
rendered yet (deep links), fade the icon in on load instead of popping, and
fall back to the generic app icon on a failed fetch.
Pasted (copied or cut) items now land selected, the same treatment
uploads get. copy_clipboard_items resolves with the created items'
paths instead of firing and forgetting, moveClipboardItems returns
each move response's authoritative final path (with Keep Both the
landed name differs from the source's), and both dashboard paste
entry points refresh with strong consistency and hand the new paths
to selectUploadedRows. Pastes into a folder that isn't rendered match
no rows and the highlight is a no-op.
Pasting or moving onto an existing name only offered Replace or
Cancel. Add a macOS-style middle ground: Keep Both retries the
operation with dedupe_name, landing the item as "name (1).ext" and
leaving the existing item untouched.
- puter-js move.js now forwards dedupeName to the wire (copy already
did; move dropped it).
- All four conflict dialogs offer the new button: copy_clipboard_items,
copy_items and move_items in helpers.js, and the dashboard's
moveClipboardItems. Multi-item selections show
Replace / Replace all / Keep Both / Skip.
- New keep_both translation key (other locales fall back to English).
Renaming (or moving) an entry left the OLD path's cache key serving
the pre-update entry for the full 60s TTL: updateEntry invalidated
only keys derived from the updated row, whose path is already the new
one. Pasting an item under the freed name then hit the stale cache in
the collision check and reported a conflict for a file that no longer
exists — and accepting the Replace it offered deleted the renamed file,
since the stale entry carries its uuid.
Read the pre-update entry when the patch changes the path and
invalidate its keys alongside the new ones. The move flow was shielded
by the outer.gui.item.moved cache-invalidation handler at the
controller layer; rename had no such band-aid, and fixing the store
covers every caller regardless of which events fire.
A copied fsentry kept its source's thumbnail pointer verbatim, so both
rows shared one S3 thumbnail object. fs.remove.node deletes the
pointed-to object, so the first removal among the sharers — e.g. the
remove performed by a replace-on-copy — broke every other sharer's
thumbnail. FSService already emits fs.copy.node for exactly this
reason (its doc comment describes the duplication), but the thumbnails
extension never subscribed to it.
Add the missing handler: S3-copy the thumbnail to a freshly minted key
and repoint the copied row, under the same only-keys-we-minted trust
rule as the read/remove paths. If the shared object is already gone
(pre-fix damage), null the pointer instead of leaving the row
advertising a thumbnail it doesn't have.
* fix: progress window covering paste conflict dialog; ghost row after Replace
Pasting a copied file onto a name collision buried the Replace/Cancel
dialog under a stuck 'Preparing...' progress window, and answering
Replace left a stale duplicate row in every client.
- helpers.js: copy_clipboard_items armed its delayed progress window
with a 0ms timer (its siblings use 2s), so the window opened
instantly over the dialog. Use 2s, and in all three of
copy_clipboard_items / copy_items / move_items pause the timer while
a conflict dialog (or the own-location / trash-deny alerts) is
waiting for input, re-arming it after. A window that opens
mid-operation now shows the current file instead of a stuck
'Preparing...', and the trash-deny bail no longer leaks a timer that
opened an orphan window after the operation ended.
- LegacyFSController: /copy and /move dropped the legacy 'overwritten'
response field and never emitted item.removed for the entry an
overwrite deleted, so clients kept a ghost row until re-listing.
Resolve the entry before the operation, return it, and emit
item.removed on success.
- helpers.js: copy_items read resp[0].overwritten but removed
resp.overwritten (always undefined), and the data-uid cleanup
selectors were unquoted — invalid CSS when a UUID starts with a
digit. Fixed all three sites.
* test: pin the v1 overwrite/collision wire contract for move and copy
The collision tests asserted only statusCode 409, which is how the
item_with_same_name_exists code regressed to 'conflict' unnoticed and
broke every replace/skip prompt in the GUI. Assert the legacy code and
entry_name explicitly, and add controller tests for the overwrite path:
the replaced entry must ride along as 'overwritten' in the /copy and
/move responses and be announced via outer.gui.item.removed so clients
drop its row.
Cut+paste onto a folder containing an item with the same name failed
with no feedback: the dashboard's moveClipboardItems swallowed the
error into console.error and cleared the clipboard, and the rewritten
backend broke the v1 wire contract the GUI's conflict prompts key on.
- FSService: move() and copy() collisions again return
item_with_same_name_exists + entry_name (the contract the write path
already preserves) instead of a generic 'conflict' code. This also
restores the desktop's existing Replace/Skip dialogs, which were
silently broken against the new backend.
- Dashboard: moveClipboardItems now mirrors the desktop's move_items
conflict flow (Replace / Replace all / Skip / Cancel, retry with
overwrite) and surfaces other move errors in an alert.
- keyboard.js: the desktop's global Ctrl+V handler threw an uncaught
TypeError on every paste in dashboard mode (its file container has
no data-path); guard it — which also prevents a double-paste if
dashboard containers ever gain one.
Re-rendering the directory already on screen (after an upload, sort
change, undo, etc.) used to clear the list and show a spinner before
the readdir round-trip, blanking the pane for the whole fetch. Keep
the current rows visible until the fresh listing arrives, then swap
the DOM in one pass and restore the scroll position. Navigation to a
different directory still clears immediately.
Reorders and updates the recommended apps list: removes 'butler', 'code', and 'traffic-tap-puzzle'; adds 'contacts', 'diagram', and 'basketball-tap' (relocated from end); adjusts overall ordering of several apps.
Update `.dashboard-settings-card .button` to use dashboard theme tokens for text, background, and borders, and add clearer hover/focus/active states with subtle elevation and transitions. Also override disabled styles in this context so dark mode no longer shows hard-coded light greys from the base `.button:disabled` rule.
Long-press-to-drag can't be made reliable on touch: touch-action is
consulted at gesture start, so the pager's pan-x claims the finger
before a drag can begin (worst on iOS). Replace it with an explicit
edit mode - a cog button (touch-primary devices only) enters it, tiles
jiggle and drag on first movement, iOS-style x badges uninstall, and
Done exits. Drops still persist immediately, same as desktop.
Also raise the drag ghost above the window z-index bands; it was
rendering invisibly behind the fullpage dashboard window.
Express 5's app.listen wraps the listen callback in once() and also
invokes it on 'error', with the error as the first argument — at which
point server.address() is null, so the startup log threw a TypeError
and killed the process before the EADDRINUSE handler could try the
next port. Bail out of the callback when it receives an error and let
the 'error' listener own the retry.
Verified: with 4000 occupied the server now logs the retry and comes
up on 4001; with the port free it binds 4000 as before.
- npm start --server=puter.com (or -- --server=...) skips the local backend
and serves the bundled GUI locally, pointed at the remote server's API.
Bare domains resolve to https://api.<domain>; full origins are used
verbatim. gui_origin points at the remote origin so /whoarewe, login,
anti-csrf, socket.io, and builtin apps hit the real backend (CORS-open).
- --extensions=<dir>[;<dir>...] bundles out-of-tree GUI extension
directories (sugar for PUTER_GUI_EXTENSION_PATHS). Their imports resolve
as if the files lived in src/gui/src/extensions, with the extension's own
files taking precedence, and bare imports fall back to the repo-root
node_modules.
- Fix the bit-rotted dev-server: Express 5 wildcard routes, pass gui()
params (previously called with none), inject the service_script shim,
load bundle.min.js + bundle.min.css in prod mode, serve /sdk, and
properly await the webpack build (it previously resolved immediately).
For extensions with no intentional built-in mapping (doc, docx, and
every other unmapped type), suggestionsForExtension fell back to
['editor'], and #resolveForExtension always placed built-ins ahead of
apps from app_filetype_association. Since suggested[0] drives the GUI's
double-click open path and /open_item, a .docx defaulted to opening as
plain text in editor even when a word processor explicitly registered
the extension.
Tag the unknown-extension result as a fallback and order third-party
filetype-association apps ahead of it. Intentional mappings (code, txt,
md, images, pdf, media) keep built-ins in the head slot as before, and
the editor guess still appears as a last-resort option.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Apps: normalize filetype associations to bare lowercase extensions
Suggested-apps lookups match app_filetype_association rows against the
bare lowercase extension ('docx'), but writes stored whatever the
developer typed. Rows like '.docx' never matched, so those apps
silently dropped out of Open With suggestions.
AppStore now canonicalizes on write (trim, lowercase, strip leading
dots, dedupe, drop empties) and tolerates the dotted legacy form on
read: getAppsByFiletype normalizes the requested extension, matches
both 'docx' and '.docx', and dedupes apps associated under both forms.
Cache invalidation keys are normalized the same way. Existing dotted
rows work without a data migration.
* Update apps tests for extension canonicalization
Adjust apps API tests to match current normalization behavior for `filetypeAssociations`: extension values are stored as lowercase bare extensions (e.g. `.txt` -> `txt`), while MIME types remain unchanged. Added inline comments in both test suites to document this expected remap.
An app launched by another app minimizes its launcher (iOS-style
takeover). Closing the child then popped onto the launcher's history
entry, and the popstate handler restored it — so quitting an app threw
the user into a different full-screen app they had not asked for, zoom
animation and all. Closing an app should go home.
Mark the launcher when it is minimized FOR a child
(data-minimized_for_child). If it still carries that mark when the child
closes, the close's single history hop lands on the dashboard instead:
the entry is rewritten to the dashboard's route and the launcher is left
minimized, still running behind its tile's running dot. No extra history
traversal — the joint session history an app's iframe shares is exactly
what pop_dashboard_app_url's watchdog exists to survive, so the hop
count is unchanged.
Back is untouched and still returns to the launcher — that is the
navigation gesture, whereas close dismisses. Any restore (Back, tile
click, Forward) clears the mark, so quitting a child after the user has
brought the launcher back leaves the launcher alone.
In dashboard mode apps are full-tab experiences, but an app launched by
another app (puter.ui.launchApp) opened as a floating titlebar window
over its full-tab parent: the parent stayed on screen around its edges,
couldn't be raised (focusWindow never raises stay_on_top windows), and a
child with no Apps-tab tile had no switcher to come back to once
minimized.
Give child launches the same treatment as tile launches — maximized, so
they get the headless chrome and control drawer — and minimize the
parent behind them, iOS-style. State only: the parent's /app/<name>
history entry already sits beneath the child's, so Back from the child
(and the child's close, which consumes its own entry) lands on the
parent's entry and the existing popstate handler restores it. The parent
keeps running while hidden, so parent/child IPC is unaffected.
Explorer keeps its windowed form, background apps don't minimize their
parent, and an explicit `maximized` option still wins. Desktop mode is
untouched: both changes are gated on is_dashboard_mode, so an app
launching an app there still gets a floating child over a visible
parent.
focusWindow re-raised a focused window's parent (and children) with the
bare z counter. For a file dialog owned by a fullpage/dashboard app that
DEMOTED the app out of the 99999999+ stay-on-top band it was created in,
burying app and dialog under every other open app window — opening a
file picker from an app stacked over another (e.g. an app launched from
Dev Center) made both vanish beneath the window below. The dialog itself
had the same flaw: raised into the plain counter band, under every app.
Raise each window within its own stacking band instead: stay-on-top
windows (and windows hanging off one through parent_uuid, walked up the
chain) get 99999999 + counter; everything else keeps the bare counter —
so desktop stacking arithmetic is unchanged. Same demotion family as the
showWindow restore fix in #3427.
Every path rendered as "> Puter > user > ...", but the root crumb is
noise on anything beneath it. Render it only at the root itself, where
it's all there is to show (the Up button can still reach /, and an
empty breadcrumb bar there would strand the user). Each crumb keeps
its leading caret, including the first.
The global Enter handler preventDefault'ed unconditionally, then
returned without acting unless a launch-menu item, context-menu item,
or selected item existed. That suppressed the browser's native Enter
activation on whatever was focused — a focused link or button (e.g. a
dashboard sidebar item, a dialog button) did nothing on Enter.
Move preventDefault/stopPropagation into the branches that actually
handle the key; otherwise fall through so native activation runs. The
handled paths are unchanged: Enter still opens the selected item,
launch-menu entry, and context-menu entry.
Sidebar items (and the user/collapse/close buttons) had no
:focus-visible style, so keyboard focus painted the UA's default blue
ring — hard-edged and bleeding outside the item. Use the dashboard's
own selection ring instead, tucked inside the control so it hugs the
rounded corners. :focus-visible only, so mouse clicks never paint it.
In dashboard mode there is no taskbar, so a minimized app window's only
switcher was its Apps-tab tile — invisible from the Files tab. Opening a
file, minimizing, and clicking the file again launched a second instance
of the app, stranding the first (and any unsaved edits) somewhere
unreachable.
Make the file row itself the switcher:
- launch_app stamps the opened file's uid on the window (data-file_uid;
the signature's uid wins so shortcuts resolve to their target)
- open_item (dashboard mode) restores/focuses an existing window that
has the file open instead of launching a duplicate, mirroring the
Apps-tab tile's single-instance behavior — keyed by file, not app, so
opening a different file still gets its own instance
- re-clicks while a launch's fetches are still in flight are swallowed
(same idea as TabApps._launchingApps, keyed by file uid, TTL'd so a
failed launch can't swallow clicks forever)
- dashboard file opens now default to maximized, so they get the same
headless full-tab chrome + control drawer as tile launches instead of
a floating titlebar window
- rows show a dot while their file is open in a (possibly minimized)
window — under the name in grid view, inline after it in list view —
driven by the existing dashboard-app-windows-changed event
Desktop mode is untouched: the reuse branch and maximized default are
gated on is_dashboard_mode, and opening the same file twice there still
creates two windows as before.
createAnswer ran outside the connection null-check, so stale or unknown
offer ids threw on the signalling socket. Guard the whole offer path and
return the #message promise from onmessage so callers can observe errors.
File row overflow menus in the dashboard now open anchored to the ⋯ button (below it with right-edge alignment) instead of pointer position, improving placement consistency. UIContextMenu now supports `position.right` for right-edge pinning, and the ⋯ trigger keeps an active visual state while its menu is open so hover styling doesn’t drop when the cursor moves onto the menu.
Safari ignores user-select: none for cursor styling, showing a text I-beam over file rows. Setting cursor: default explicitly fixes this and is inherited by child elements.
Match speech2txt/speech2speech and reject on decoded payload size, not
data-URI string length. Base64 expands ~4/3, so string-length checks
falsely rejected valid images under the 10MB limit.
Completions-family providers used truthy spreads, so temperature: 0 and
max_tokens: 0 were stripped before the SDK call. Align with Responses
providers that already use !== undefined.
* feat: add Speechify TTS driver
Adds a SpeechifyTTSProvider under the ai-tts driver (mirrors the xAI/
ElevenLabs REST-provider shape), registered in TTSDriver alongside the
existing providers. Wires puter-js txt2speech with provider: "speechify"
support, default voice/model, and the speechify-tts driver alias.
Every outbound request sets Speechify-Caller: puter; base URL is
https://api.speechify.ai only; default model is simba-3.2.
* fix: replace placeholder voice IDs with real simba-3.2/simba-english voices
DEFAULT_VOICE and the starter voice catalog used invalid IDs (henry,
cliff, kristy, george, aria) that the real API rejects with 400. Swapped
in confirmed-real voices from a live GET /v1/voices call: geffen_32,
dominic_32, harper_32, hugh_32, imogen_32, and alec for the
simba-english model-override test.
* fix: purge remaining placeholder voice IDs from client, docs, and types
The previous fix only covered the backend provider — the client-side
default in tts.js, its test, the txt2speech docs page, and the ai.d.ts
type comment all still referenced the invalid henry/cliff/kristy/george/
aria set. All replaced with the live-verified voices (geffen_32 default).
Also corrects the docs link to docs.speechify.ai.
Adds Infron (https://infron.ai) as an aggregator provider for the
puter-chat-completion driver, following the OpenRouter provider as
reference per doc/contributing-apis.md.
- OpenAI-compatible gateway at llm.onerouter.pro/v1
- Dynamic model catalog (kv-cached 15 min, authenticated fetch),
filtered to chat-capable LLM entries
- Bills the gateway-reported authoritative cost when present
(usage: {include: true}); falls back to catalog per-token pricing
- Registered as an aggregator so its aliases never shadow
first-party providers
- Offline unit tests (mocked SDK/axios against a real test server)
plus an env-gated integration test
* Rework permission requests: new dialog, working popup flow for websites
- Replace the UIWindow-based permission prompt with a standalone top-layer
<dialog> (responsive, light/dark, app/site identity, input protection)
- Implement puter.ui.requestPermission for env=web: opens the GUI's
/action/request-permission popup with pinned origin/source/msg_id,
popup-closed detection, and a check-permissions polling fallback for
crossOriginIsolated openers
- Move the GUI's request-permission action into postAuthActions so
signed-out users sign in first; identify the app by opener origin,
correlate responses with original_msg_id, close the popup after answering
- Always respond from the IPC handler so the SDK promise can't hang;
normalize the result to a strict boolean (a failed grant no longer
resolves truthy)
- Accept origin in /auth/grant-user-app and /auth/revoke-user-app,
mirroring grant-dev-app (fixes puter.perms.grantOrigin/revokeOrigin)
- Add Playwright e2e coverage for both the desktop and popup flows;
update e2e harness for the auth_token_v2 localStorage key
- Update types and docs (perms request methods now work on websites)
* Harden permission request flows
- Grace period before treating popup close as denial: the GUI posts the
decision then closes the popup, and postMessage delivery is not ordered
relative to `closed` becoming true, so a grant could race to a false
- Unique popup window name per request so window.open name-reuse can't
hijack a still-pending request's popup
- request-permission action always answers the requester and closes the
popup, even when app resolution or the dialog throws
- Permission dialog: refuse unidentifiable requesters, allowlist icon URL
schemes, and time out the grant request into the retryable error path
- Validate app_uid/origin/permission types and length in grant-user-app
and revoke-user-app
- Tests: revoke-by-origin and input-validation backend tests; e2e for
dialog dedup, unsupported permissions, and the consent-dialog path
* Sign in first in the requestPermission fixture's email flow on the web
In env=web the site has no auth token, so whoami threw 401 immediately
and the email button appeared to do nothing. Sign in via popup first,
matching the real third-party flow; env=app already has a token and is
unaffected.
* Point the requestPermission fixture at the real api subdomain
The SDK sends credentialed CORS requests; the GUI host doesn't answer
with Access-Control-Allow-Credentials, so whoami (and any authed call)
from the fixture origin failed CORS and looped through retries.
* Fix /auth/list-permissions schema mismatches
The endpoint's queries referenced columns that don't exist:
user_to_app_permissions stores a numeric app_id FK (not app_uid), and
user_to_user_permissions uses holder_user_id (not target_user_id) —
every call 500'd. Join apps to expose the app's uid and use the real
column names.
Replace the catch-either-branch test (which documented the breakage
instead of failing on it) with real assertions covering all three
sections of the response.
* Identify permission requesters by origin only
The request-permission action took `app_uid` straight from the query
string and used it as the grant target whenever the origin was absent or
unresolvable. Now that /auth/grant-user-app accepts `origin` and prefers
`app_uid` when both arrive, the displayed identity and the grant target
could diverge; with only `app_uid` in the URL the dialog rendered with an
empty name, so a link could produce a bare "Allow" prompt for an unnamed
requester. Resolve the uid from the origin alone, and let the server
resolve it from that same origin when the client lookup fails.
Also give the no-gesture consent popup a unique window name. UI.js does
this on the direct path because window.open() reuses a window with a
matching name, but the PuterDialog fallback opened under the default
'Puter' — the same name sign-in uses, so a consent click could navigate
an in-progress sign-in popup away.
And guard the IPC responder: an app that closes its own window while the
dialog is up leaves target_iframe.contentWindow null.
* Keep the permission popup from signing the site in
The popup loads the GUI with embedded_in_popup=true, so it ran the
sign-in token exchange and posted puter.token to the opener before the
user answered the prompt. A site that called requestPermission() walked
away holding a user-app token for the account even when the user pressed
"Don't Allow" — and because the SDK's global puter.token handler feeds
event.data.token into setAuthToken() without looking at `success`, a
failed exchange posted token: null and wiped a token the site already
had. Keep running the exchange (it bootstraps the app row the grant
needs and caches host_app_uid) but leave the token in the popup. A site
that wants credentials still has to call signIn().
Escape on the SDK's consent dialog left the caller pending forever.
PuterDialog wired its Cancel and close buttons but not the <dialog>'s
native cancel event, so the browser dismissed the dialog and nothing
reported it: no dialog, no popup, no answer. Route cancel to the same
handler. Programmatic close() fires only `close`, so launching the popup
— which closes this dialog — is unaffected, and the implicit-auth flow
stops hanging on Escape too.
Serialize the permission dialogs. showModal() makes the whole document
inert rather than just the requesting app's window (which is what the
UIWindow it replaced did), and the dedup map only coalesced identical
requests, so an app asking for permissions in a loop stacked one modal
per request and walled the user off from the desktop — including from
the app doing it. Prompts now queue and open one at a time, and each
caller still gets its own decision.
Identify apps by more than their title. `title` is free-form text the
author picks and is not unique, so it was the whole identity of a prompt
an app titled "Puter Settings" could raise; the registered `name` is
unique and format-restricted, so show it underneath. Give the name line
the unicode-bidi isolation the origin line already had, since escaping
leaves bidi overrides intact.
Stop the dialog from answering over its own in-flight grant: a dismissal
while the POST was outstanding resolved false for a permission the server
was committing. Ignore dismissals while granting, and time-box the
request with AbortController (AbortSignal.timeout isn't everywhere) so a
hung network can't leave a modal no one can close. Fail closed on the
remaining paths that could reject or prompt uselessly — showModal()
throwing under <iframe sandbox>, and a requester known only by app_name,
whose Allow the server would always reject. Pass the error string to
.text() unencoded so translations containing an apostrophe don't render
'.
The e2e suite covers all of it; each new test fails without its fix.
* Restrict the permission popup flow to third-party websites
requestPermission's new web path ran in every environment with a window,
including env='gui' — so a permission_denied driver retry inside the
Puter GUI would open a popup to the Puter origin from the desktop itself
and try to grant the permission to a phantom app for Puter's own origin.
Resolve false everywhere except env='web', the previous behavior.
* Settle the permission dialog when a failed grant has no dialog left
The cancel handler is preventDefault'd, but close requests can't be
suppressed forever: Chrome's close watcher lets a repeated Esc skip
cancel and force-close the dialog while the grant POST is in flight.
The close handler defers to that grant on purpose — but if the grant
then failed, fail_grant re-enabled buttons on a closed dialog and the
promise never settled, leaving the requesting app waiting forever.
Settle as a denial when the dialog is no longer open.
Also add regression tests for this and for the popup-flow env guard
(routing the CDN SDK URL to the local build, since the prod-built GUI
loads its SDK from js.puter.com).
* Withhold the auth token on the popup's first-visit paths
Keeping the token inside the permission popup only covered the plain
token exchange. Two other popup paths mint a user-app token and posted
it to the opener unconditionally: first-visit temp-user creation, and
the manual signup shown when temp users are refused. Both sit on the
path a brand-new visitor takes — the audience the website popup flow
exists for — so a site that asked about one permission and was denied
still walked away holding a token, for a temp account or a real one.
The SDK's global puter.token handler feeds whatever arrives straight
into setAuthToken(), so posting it is the whole of it.
Move the rule into util/popupAuth.js and consult it at every site that
posts the token, so the next token path has one place to ask.
The first-visit path also left the prompt itself unreachable: it waits
on the spinner promise, which only resolves when the spinner was up for
under 2s. End that wait for any action that keeps the popup open;
sign-in still closes the window as before.
The e2e test fails without the fix — the site holds a token after the
user presses "Don't Allow".
* Poll for the decision when the popup's opener is severed
crossOriginIsolated was the test for "the popup can't message us back",
but being isolated also requires COEP. A site sending COOP: same-origin
on its own still has its opener relationship severed when it opens the
Puter popup, and took the watch-the-window path instead — where the
detached proxy reports closed === true on the first tick, so
requestPermission resolved false about a second after the popup opened,
while the user was still reading the dialog. Their "Allow" then had
nowhere to go. Treat an already-closed popup as severed and poll.
Pin the expected event.source before those early returns. popupWindow
was assigned after them, so for the whole consent-dialog wait — as long
as the user takes to click Continue — the handler accepted a decision
from any window on the GUI origin. A forged answer is only advisory
since the grant is written server-side, but the check may as well hold.
Settle instead of rejecting when the consent dialog can't be appended:
document.body is null in a <head> script, and the throw both rejected a
promise documented to resolve to a boolean and left the message listener
behind.
* Key the dialog dedup by the identity its gate accepts
The gate treats an empty app_uid as absent and falls through to the
origin; the dedup key used ?? and kept the empty string, so two requests
from different origins would collide on one key and share a single
decision. No caller can produce a blank uid today — server uids are
never empty and the IPC path's empty attribute is stopped by the gate —
but the two lines have to agree.
* Close the gaps the permission-request flow left open
Seven defects found reviewing the new permission flow end to end, each
reproduced against a running server before being fixed.
Security:
- `cross_origin_isolated=true` bypassed `deliversTokenToOpener` entirely.
That branch is checked first, mints a user-app token, publishes it via
`/login/set` and returns — so one query parameter on a
request-permission URL skipped the prompt and handed the opener a token
through the unauthenticated `/login/wait`. Gate it with the same rule.
- Grant/revoke by `origin` could land on an unrelated app. An origin with
no app row synthesises `app-<uuidv5>`, and the permission services
resolve their identifier as uid *or name* — and the uuid namespace is a
source constant, so the string is computable offline and registrable as
an app name. Resolve origins to a uid that names a real app row.
- A website's host was elided on the right, hiding the registrable domain
that says who is asking. Elide it from the left, as the sibling rule
already intended.
- A grant whose response was lost (client-side abort, dropped reply) left
the row committed while the dialog reported a denial. Withdraw it when
the user then answers "Don't Allow".
Correctness:
- `pollDecision` needs the site's own token, which a permission popup
deliberately never delivers, so a signed-out cross-origin-isolated site
burned the full five-minute timeout before answering. Answer at once
when there is nothing to poll with.
- `getUserAppToken` reports failure by returning null, and three callers
read `.app_uid` off it. Guard all three, keep the first-visit spinner
promise settling on its failure paths, and dispatch the `login` event on
the manual-signup path so `postAuthActions` runs at all — a user who
signed up inside a permission popup got a blank window and the site got
no answer.
- Time-box the lookups that run while a request holds the dialog queue's
slot: they have no timeout of their own, and a stall (not a failure)
wedged every later permission request in the page.
Also harden the grant/revoke input validation the PR introduced — it
skipped `extra`/`meta`, so a non-object faulted *after* the row was
written, and its length cap was 16x the column it lands in — stop a
non-URL `origin` from throwing past the answer-and-close, and drop the CSS
left behind by the deleted dialog.
* Close three gaps left in the permission-request flow
Each was reproduced first — the squatting grant against a running server,
the COOP timing in a real browser — and each fix was then confirmed by
reverting it and watching the new test fail.
Security: the dialog could name one site and grant to another.
The squatter guard added for grant/revoke by `origin` only ran when
`app_uid` was absent, and the dialog sends both — so `app_uid` won and the
guard never applied. `getAppUIDFromOrigin` returns the synthetic
`app-<uuidv5(origin)>` for any origin with no app row of its own, and the
grant endpoint resolves `app_uid` as uid *or name*, so the grant landed on
whoever registered an app under that computed name (the format allows it,
and the namespace is a source constant). A link like
`/action/request-permission?origin=https://a-site-you-trust.example` named
that site in the prompt while "Allow" handed the permission elsewhere.
Fixed on both sides of the wire. The action now sends the origin alone —
no uid resolved in the browser is safe to forward, whatever its source —
and a supplied `origin` now decides the target on the server even when an
`app_uid` travels beside it: the origin is what the prompt showed the
user, so it is what the grant has to follow.
Correctness: a COOP-only site was answered before the user decided.
7efc0c0b routed a severed opener to `pollDecision` by treating an
already-closed popup as severed, but that reads `closed` synchronously
after `window.open()` — before the navigation whose response headers cause
the severing has committed. Measured in Chromium: `closed` is false at
0ms and true by 200ms. So a site sending COOP: same-origin without COEP
still took the watch-the-window path, and `requestPermission` resolved
false 1.1s after the click while the dialog was still on screen. The
"Allow" that followed committed a grant the site had been told it did not
get. Tell the two apart by when the close lands, and keep the in-flight
message's grace period on both branches — an answer already on its way
outranks whatever the close is taken to mean.
Correctness: a grant that timed out was not withdrawn.
`grant_may_have_committed` was only set in the fetch's `catch`, which
cannot run until the timeout's timer callback returns — and that callback
already calls `fail_grant`, which settles the dialog as a denial outright
when the dialog was force-closed mid-grant. The reconciliation was
skipped in exactly the case it exists for. Record the unknown outcome in
the timer, where the timeout already means the request left the browser.
Also require a `token` from the user-app exchange rather than just a
non-null body: an HTTP failure (a blocked origin, a 5xx) returns the
parsed *error* body, which is truthy, so the guard added for this missed
it — handing the opener an `undefined` token, and prompting for a grant
whose app row was never bootstrapped.
Known limitation, now more reachable: a severed opener cannot signal a
denial at all, since nothing is written for one, so those sites wait out
the poll timeout before receiving false. A grant still resolves promptly.
* Deliver the uncertain-grant withdrawal from a closing popup
The permission dialog reconciles a denial after an uncertain grant by
firing a revoke in the background, but the popup flow posts the answer
and closes the window right after settling — and a plain fetch is
cancelled with its document, so the withdrawal never reached the server
and the user was told "denied" while the grant stayed live. Send it
with keepalive so the browser delivers it independently of the popup,
and cover the popup flow with a regression test (the existing
withdrawal tests only exercise the desktop flow, where the GUI
outlives the dialog).
Also make the popup boot's getAppUIDFromOrigin guard functional: the
helper reports failure by resolving to a null/undefined uid, not by
throwing, so the catch never engaged and a failed lookup clobbered
window.host_app_uid with undefined despite the comment claiming the
token exchange's value was kept.
* Keep the ai-chat model-map build inside the server lifecycle
onServerStart fired #buildModelMap without awaiting or tracking it, so
the network fetches it does (notably Ollama auto-discovery, which is
enabled by default and doomed on any machine without a local Ollama)
kept running after server.shutdown() resolved. In vitest that let the
provider's console.error land during worker teardown, which surfaces as
"Closing rpc while onUserConsoleLog was pending" — the unhandled error
that intermittently fails CI (last seen attributed to
WispController.test.ts). A rejection in the detached chain would also
have been an unhandled rejection.
Track the promise, catch and log rejections, and await it from
onServerShutdown so no provider I/O or logging outlives the server.
Also disable Ollama auto-discovery in setupTestServer's defaults —
every test server was firing a pointless model-list fetch at localhost.
* Settle requestPermission on the launch paths that could still throw
Three gaps left by the permission-request rework, each verified against a
live stack before and after the fix.
The IPC handler normalises a non-object `options` so it can always reply,
but `typeof null === 'object'` let null through the guard; reading
`.permission` off it threw out of the message listener before any reply,
so `puter.ui.requestPermission(null)` hung forever in env=app while
env=web answered false for the same input.
In the env=web branch only the consent-dialog path was wrapped, even
though its own catch reasons that this resolves to a boolean for every
other caller. A `window.open` refused by throwing rather than by
returning null escaped the launch branch and rejected instead.
The perms docs were flipped to platforms: [websites, apps], but every
entry point except `request()` reads the signed-in user's identity
first, so on a signed-out site they reject with Unauthorized and never
prompt — the permission popup deliberately does not sign the site in.
Document the sign-in precondition on those pages.
* Measure a permission's width after the rewrite that decides it
The new grant validation capped `permission` at 255 to match the column
it lands in, but it measured the caller's raw string. `fs:/path:mode` is
rewritten to `fs:<uuid>:mode` before storage, so what lands in the column
is ~44 characters however deep the path is. Granting access to a deeply
nested file therefore returned 400 even though the identical target
granted by uuid returned 200 and stored 44 characters — and because a 4xx
is read as an outright refusal, the permission dialog showed its
retryable error and could never succeed on retry.
Bound the request body only against absurd input, and enforce the column
width in the permission service on the rewritten string, before the app
is resolved so an oversized permission still refuses ahead of a missing
app. Covered both ways: a rewritten-short permission is accepted, and one
that no rewriter shortens is still refused.
* Require a registered app when a dev-app grant names an origin
The user-app handlers resolve a caller-supplied origin through
#registeredAppUidFromOrigin because appUidFromOrigin synthesises
app-<uuidv5(origin)> for an origin with no app row, and the permission
services resolve their identifier as uid-or-name — so the synthetic uid,
derived from a published namespace constant and computable offline,
lands on whoever registered an app under that literal name. The dev-app
handlers were left resolving the raw synthetic uid.
That leg matters at least as much: a dev-app grant is scanned with the
issuer's authority for anyone running as that app, so a squatted grant
hands over the granting user's permission. Verified against the store —
the grant landed on the squatter rather than rejecting.
Without a squatter the synthetic uid resolves to nothing and these
already 404, so the guard costs the legitimate case nothing; a test
covers a registered origin still resolving to its app.
Also assert that revoke accepts the same oversized-but-rewritten
permission grant does, since the dialog's withdrawal of an uncertain
grant depends on that symmetry.
* Revoke the row a user-app grant actually wrote
`app-root-dir:<uid>:<mode>` is a pseudo-permission: its rewriter resolves
it to a real `fs:<root_uid>:<mode>` only while a user-app permission row
is being written, and resolves to a match-nothing sentinel at all other
times so a scan can't match through the fs path.
Revoke shared that rewrite but not the flag, so it aimed the DELETE at
the sentinel: it removed nothing and reported success while the fs
permission stayed live. The permission dialog withdraws a grant whose
outcome it couldn't confirm through exactly this path, so a user who
answered "Don't Allow" after a dropped grant response kept the access
they had just refused.
Grant and revoke now share one rewrite helper. It also has to work for a
caller outside a request scope — an internal job, or a direct unit test —
where `Context.set` has nothing to set the flag on; an empty scope reads
the same as no scope, so it only makes the flag settable.
* Elide a long host from the left, as its own rule intends
The identity line is the only thing on the permission dialog naming the
requester, so a host too long for the dialog has to lose its front, not
its tail: the registrable domain is the part that says who is asking.
`direction: rtl` was there for that, but paired with
`unicode-bidi: plaintext` it does nothing — plaintext takes the base
direction from the content's own first strong character, which for any
Latin host is LTR, so the ellipsis went back on the right. Measured in
the real dialog, `account-security.paypal.com.verify-login.example`
rendered as `account-security.paypal.com.verify-l…`, reading as PayPal.
Isolating instead keeps the box anchored to the end of the text, and
still stops a bidi control character in the host from reordering
anything around it.
The test that covers this asserted the computed `direction` — the
property, not the outcome — so it passed throughout. It now measures
which characters are actually on screen, and that the host still reads
in source order.
* Tell a severed opener from a closed one by whether it can answer
The web popup flow decided which it was looking at by timing: a
`popup.closed` flip within 3s of `window.open()` was COOP severing the
opener, anything later was the user closing the window. Both halves
misfire, and both were reproduced against a real popup.
A COOP-only site whose popup navigation commits after the cutoff had its
severing read as a close, so the site was told "denied" about a second
later — while the prompt was still coming up. The Allow the user went on
to click then committed a grant the site had been told it did not get,
which is the failure the cutoff was introduced to prevent: how long a
navigation takes says nothing about whether the opener survived it.
The other way round, a signed-in site whose user dismissed the popup on
sight had that close read as severing, and fell back to polling for a
decision. A denial writes nothing to poll for, so the caller waited out
the full five-minute timeout instead of being answered.
So ask the question directly: the popup now announces itself to its
opener, which it can only do while the relationship is intact. Having
heard from it proves a later close is a real close and the answer is now;
never hearing from it means the prompt may be live in a window that
cannot answer, and the decision is read back from the server as before.
The announcement carries no token, and goes out before any sign-in gate —
a gate that delayed it would make abandoning sign-in look severed.
Measured: the popup announces itself ~290ms after opening, and a close
just after that is answered in ~1.3s rather than five minutes.
* Stop revoking a literal `*` after a dev-app revoke-all
The `*` arm of /auth/revoke-dev-app fell through: after
`revokeDevAppAll` it also ran `revokeDevAppPermission(…, '*')`, a
DELETE naming a row called literally `*` — which matches nothing —
plus a second `revoke` audit entry for the same action. The user-app
twin already if/elses its two arms; the dev-app handler now matches it,
and a test pins the parity: everything revoked, one audit row.
* Match the popup's messages against a canonical GUI origin
The web flow compared `event.origin` to `puter.defaultGUIOrigin` as raw
strings, but they are different kinds of value: the event carries the
browser's canonical origin serialization, while the configured origin is
whatever text was supplied — a trailing slash, an explicit default port,
or a stray path all name the same origin and all fail the comparison.
The mismatch doesn't read as a config error, it reads as the user's
answer: with every message from the popup dropped, the missing
`permissionPromptReady` makes the popup's close look like a severed
opener, and the missing decision leaves that path to answer on its own —
"denied", for a guest, moments after the user clicked Allow and the
grant committed.
Parse the configured origin once and compare canonical-to-canonical; the
popup URL is built from the same parsed origin, so a trailing slash no
longer yields a `//action/...` path either. A configured origin that
cannot parse could never have hosted the prompt, so it now denies up
front instead of opening a broken window. Pinned by an e2e test that
re-points the SDK at the same GUI through a trailing-slash origin and
expects the grant to be heard; it fails against the raw comparison.
* Take a permission popup's requester from the browser, not the link
`app_uid` was removed from the request-permission URL because the uid
names who receives the grant, so it has to come from the requesting
origin rather than from whoever built the link. Two other parameters
still carried exactly that identity, and the origin is the identity
twice over: it is the name the dialog attributes the request to, and it
is what the server resolves into the app the grant is written against.
`opener_origin` is believed on every popup boot, ahead of the referrer.
`origin` was the fallback in the request-permission block itself,
reached whenever there is no opener at all. Either one lets a bare link
raise a consent prompt in some other app's name — the token exchange
bootstraps an app row for whatever origin was typed, so the grant then
commits against it — while the user is looking at a domain the requester
does not control. That is the whole dialog defeated: a site can name
`docs.google.com` and have the Allow land on Google's app row.
So a permission popup now takes only an origin the browser vouches for:
`document.referrer`, or the opener's own reply to the `requestOrigin`
handshake. Neither can be forged to another origin. Nothing legitimate
relied on the parameters — the SDK sends neither, and the OIDC redirect
`opener_origin` exists for drops `action` too, so no permission flow can
arrive through one. The `origin` fallback only ever fired when there was
no opener, which is to say when there was no requester either; that path
now reports its denial as usual instead of prompting.
The referrer is safe here precisely because `action` is only
`request-permission` on the popup's first load, so it is always the
opener's. Restoring the action across an OIDC hop would break that — the
returning navigation's referrer is the identity provider — which is why
the round trip is withheld rather than repaired:
A popup that signs in through OIDC comes back to a redirect URI the
server hard-codes to `/action/sign-in`, so it returns believing it is a
plain sign-in popup. It posts `puter.token`, which the SDK's global
listener feeds straight into `setAuthToken()`, and never runs the action
it was opened for. For a permission prompt that is the exact outcome
`deliversTokenToOpener` exists to prevent: the site is signed in without
ever asking, the user is never shown the permission they were brought
there to decide, and the request resolves as a denial. Nothing in the
returned URL says what the popup was for, so it cannot recover on its
own. Until the redirect can carry the action back — and the opener
origin can be re-established from the handshake on return — a popup
whose purpose cannot survive the hop does not offer the hop. Email
sign-in stays in the window and is unaffected.
Pinned by e2e tests that spoof each parameter and expect the prompt to
name the real opener, or not to appear at all. The long-hostname test
drove the dialog through `origin=`, which no longer produces one, so it
now asserts the CSS elision contract directly while the popup test
asserts the real flow applies the `perm-dialog-entity-host` class that
contract keys on.
* Refuse an origin the grant could never have named
The identity line elides a long host from the left, keeping the
registrable domain visible, which it does with `direction: rtl`. That is
sound for a host — every character in one resolves left-to-right, so the
string reads in source order — but it is not sound for arbitrary text,
whose neutral and RTL runs can render in an order they were not written
in. On the one line of this dialog whose whole job is saying who is
asking, that is the wrong thing to be lenient about.
The entity resolver reached that state through its own fallback: when
`new URL(origin)` threw it displayed the unparsed string and still marked
it as a host. An origin the server cannot parse cannot name a grant
target either — `AuthService#originFromUrl` rejects it, and rejects
non-http(s) schemes with it — so there was never anything to prompt
about. Deny at the gate, next to the existing "requester the grant can't
name" check, on the same test the server applies.
* Give the decision poll a deadline it can actually reach
`pollDecision` bounds itself with a five-minute budget, but it only reads
the clock between iterations and its `fetch` had no timeout of its own. A
request that never settles — a stalled connection, a proxy that accepts
and never answers — parks that `await` forever: the loop never comes
back round to check, `settle` is never called, and the caller's promise
stays pending for the life of the page with the message listener and
interval still attached. It is the only unconditional hang left in the
flow, and the least recoverable one, because the popup is already closed
on this branch and nothing the user does can rescue it. Each attempt now
gets ten seconds — long enough that a slow-but-working connection is
still heard, short enough that the deadline means something.
The request id changes for a related reason. It was `#messageID++`, a
small integer restarting at 1 on every page load, and `event.source` is
not pinned for as long as the no-gesture consent dialog waits for its
Continue click — a stretch of time the user paces. A permission popup
left open from before a reload posts this exact message shape to its
opener on the way out, and its counter value collides with a fresh
request's, settling it with the decision the user made about a different
permission. A random suffix makes the two impossible to confuse; the GUI
echoes the value back verbatim, which the loose comparison still handles.
* Answer the app when the signup gate throws
The requestPermission branch grew a `respond` helper so every exit tells
the app something and its promise settles instead of hanging. One exit
still doesn't: `await UIWindowSignup(...)` is outside the guarded region,
and `ipc_listener` has no outer catch, so a throw from the signup window
escapes the listener entirely — before any reply — and leaves the app
waiting forever. That is the precise failure the helper was added to rule
out. Treat it as the refusal it amounts to.
* Revert "Keep the ai-chat model-map build inside the server lifecycle"
This reverts commit 1b2482dcb8.
When dragging is aborted by returning false from the sortable 'start' handler, the cursor plugin never fires its cleanup, leaving the body cursor stuck on 'grabbing'. Reset the body cursor explicitly on each early-return path.
Trash is accessible via the sidebar and should not appear as a row in the file explorer. Filters it out from directory listings and prevents socket events from re-adding it to the view.
Add a `rubberband-selecting` state on the dashboard while marquee selection is active in `TabFiles`, and remove it when drag ends. CSS now disables pointer events on surrounding files chrome (sidebar, directory tree, and header) during that state so hover highlights/tooltips don’t trigger when the cursor leaves the clamped selection area.
Make Home consistently visible and selected in file navigation. The file tab now shows the Home directory entry and auto-selects Home (instead of Desktop) when no initial path is provided. In `UIWindow`, saved sidebar layouts are backfilled to include Home if older preferences are missing that entry, preventing users from losing quick access after upgrades.
Extract duplicated trash icon update code into two shared helpers:
- `window.update_trash_icons(is_empty)` updates all trash icon surfaces (taskbar, desktop items, shortcuts, open window headers, Dashboard sidebar) in one place.
- `window.refresh_trash_state()` stats the trash, emits the socket event, and calls `update_trash_icons`.
Replace all scattered inline stat+emit+icon-update blocks across UIItem, UIWindow, UIDesktop, UIDashboard, keyboard, helpers, and generate_file_context_menu with calls to these helpers. Also syncs the Dashboard sidebar trash icon on load and after every destructive operation.
Uploads now land selected wherever they finish: the desktop, explorer
windows, file dialogs, and the dashboard Files tab. Selection is applied
from both completion channels — the batch response selects elements
already in the DOM, and items whose item.added socket event arrives
later are remembered (with a TTL) and selected on creation. The explorer
footer count and the open-file dialog's Open button are updated to
match, and disabled items (e.g. filtered out by a dialog's file-type
filter) are never selected.
Landing on /app/<name> logged-out and authenticating used to dump the
user at the root dashboard on several paths, losing the app they came
for:
- OIDC login/signup only sent return_to for /desktop and /dashboard
(and the backend whitelist only accepted those two), so OIDC from an
app landing redirected to /.
- UIWindowSignup defaulted its post-success redirect to /, so password
signup reached via the session list, ?action=signup, or in-app signup
prompts (IPC.js) lost the app.
- OIDC error redirects always went to /?action=..., so a recovered
attempt (e.g. account-not-found bounced to signup) also lost the app.
New helpers in src/gui/src/helpers/auth_redirect.js:
- get_auth_redirect_url(): stay on the page auth started from;
/action/* pages go to /; strips action/auth_error/message/
request_code so the reload doesn't re-open the auth window or pass
auth params through to the app as launch args.
- get_oidc_return_to(): pathname when whitelistable, now including
/app/<name> (trailing slash normalized).
Backend (OIDCController):
- Shared isWhitelistedReturnPath() accepts /desktop, /dashboard, and
/app/<name> (charset mirrors APP_NAME_REGEX — no open redirect).
- buildErrorRedirectUrl() lands on the whitelisted originating page
from the signed state's redirect_uri instead of always /.
Tested: 47/47 OIDC controller tests pass (4 new: return_to accepted/
rejected on start, success redirect to /app/<name>, error redirect
keeping /app/<name>); verified live on local dev for first-visit temp
user, password signup (incl. email-confirmation gate), and password
login — all land on /app/camera with the app open.
Saving a file via puter.ui.showSaveFilePicker from a third-party website
(popup flow) consistently failed with a DataCloneError alert after
clicking Save, and saving over an existing filename showed a raw error
instead of the Replace/Cancel prompt.
Two bugs:
1. privacy_aware_path is a curried factory (world => fspath => ...), and
initgui.js is the only module that imports it directly — so the popup
save handler's privacy_aware_path(res.path) returned the inner
function, which postMessage cannot structured-clone. Every other call
site resolves the bare name to the correctly bound
window.privacy_aware_path global, which is why only the external-site
popup flow was broken. Use the global at the call site and import the
factory under a distinct name so a bare call can't silently resolve
to it again.
2. The v2 backend returns `conflict` for a same-name write, but the v1
wire contract is `item_with_same_name_exists` + `entry_name`, which
the GUI's save dialogs key on to offer the overwrite prompt. Restore
the legacy code/field on the write-conflict error and carry
HttpError.fields through the /batch per-op error serializer.
Verified end-to-end against a local backend: fresh save resolves the
caller's promise with the signed saved_file and closes the popup;
saving an existing name shows Replace/Cancel and Replace overwrites.
Backend suite shows no new failures.
The cover-page rule added for the auth windows forced `width: 100%` on
every `.window-cover-page .window-body`, including the popup file,
directory, and save pickers. That body is an item-container with
`overflow: auto`, so it establishes its own formatting context and may
not overlap the floated `.window-sidebar`. Unable to shrink to fit, it
dropped below the sidebar, pushing the file list and the Cancel/Open bar
outside the 700x400 popup and leaving the page background showing
through as grey.
`.window-body-filedialog { width: auto !important }` was meant to cover
this but loses on specificity, one class against two. Scope the auth
rule with `:not(.window-filedialog)` instead; that class is only set for
the three dialog kinds, so no auth or session cover page changes.
Extracted desktop `item.added` container update logic into a shared `apply_item_added_to_containers` helper and wired it into both `UIDesktop` and `UIDashboard`. The helper handles overwrite updates, new item insertion, icon refresh, and resorting for matching item containers. This fixes dashboard-hosted explorer/file dialogs not reflecting uploads until navigation.
Replace reuse of `#upload-file-dialog` with a dedicated hidden file input owned by `init_upload_using_dialog`. This avoids collisions with the dashboard Files tab’s `onchange` property handler, which could trigger duplicate concurrent uploads and "Entry already exists" races. The new flow stores the current target path once per dialog open, clears input value before click (so re-selecting the same file still fires `change`), and snapshots `FileList` to an array before clearing to keep async uploads reliable.
- Replace the full-tile drag ghost with a compact icon+name chip:
stacked sheets and a count badge for multi-selections, dimmed
source rows, same design in list and grid views.
- Holding a dragged item over back/forward/up or a breadcrumb
segment now spring-navigates there (700ms dwell); cancelling
the drag returns to the original directory.
Selected tiles/rows now use a subtle accent tint plus hairline ring
instead of a solid --select-color fill, so icons and labels keep their
natural colors in both themes. Also refines the grid tile's hover
'...' button (inset, hairline border, hover state), fixes the
light-mode selected-row text color that referenced a dark-mode-only
variable, and drops the opaque icon chip visible on tinted list rows.
Refactors the dashboard’s mobile sidebar into a modal-style drawer with a coordinated state helper, scrim overlay, and dedicated close button. Adds accessibility improvements (`aria-label`, `aria-expanded`) and stronger dismissal behavior (scrim tap, close button, Escape, and swipe-left with velocity/threshold handling). Updates mobile CSS for safe-area-aware positioning, improved touch targets/pressed states, frosted toggle styling, drawer shadows/transitions, and drag-state handling for smoother interactions.
The minimize controls consumed the app's /app/<name> history entry
first and let the popstate handler do the visible hide. When the app's
iframe had stacked joint session-history entries, the pop only settles
at pop_dashboard_app_url's 400ms watchdog — so clicking minimize sat
still for ~half a second before the morph began.
Hide the window in the click frame instead (minimize_window, shared by
the head button, the context menu, and the control drawer) and demote
the pop to pure URL bookkeeping. The eager hide is safe against both
ways the pop settles: every hideWindow branch marks data-is_minimized
synchronously, and the popstate handler and the watchdog both already
skip minimized windows. The browser's Back button still minimizes
through the popstate handler as before.
Minimize (and close's URL cleanup) on headless dashboard app windows
work by consuming the app's /app/<name> history entry with
history.back() and letting the popstate handler do the actual hide.
But back() traverses the JOINT session history, which the app's iframe
shares: if the app navigated internally after load (SPA router,
redirects — and browsers differ on which iframe navigations stack
joint entries), the back() consumes the IFRAME's entry instead. The
parent's URL never changes, no popstate arrives, and the
dashboard_url_pop_pending latch stays stuck — every later minimize and
close becomes a silent no-op, and the stranded /app/<name> entry makes
later traversals resurrect windows the user already dismissed.
Fix with a watchdog after each back(): a successful pop is a
same-document traversal of the parent (app entries are pushState over
the dashboard route), so its popstate lands well inside 400ms and the
watchdog no-ops. If the URL still names the app by then, do what the
popstate would have done — minimize through the same lookup and
minimized-guard — and replaceState the stranded entry back to the
dashboard's own route (captured at push time, tab hash included) so no
stale entry survives.
* Dashboard: redesign app control pill as a top-edge drawer
The floating control capsule over headless dashboard apps consumed
noticeable space, especially on mobile. Replace it with a drawer: a
slim glass tray flush with the top edge (icon + title + minimize +
close) whose tongue-shaped handle hangs from its bottom center. At
rest only the tongue peeks in from the edge; hovering it (mouse),
tapping it, or keyboard-focusing the drawer slides the tray down.
The collapse is a single GPU transform instead of the pill's
max-width squeezes, and the open-then-retract intro now visibly
retracts into the tongue, teaching where the controls live.
Hover-expansion is gated on pointerType === 'mouse': touch taps
synthesize a pointerenter right before click, which made the old
pill's toggle see an already-open state and shut it again.
Also fixes two window-stacking bugs surfaced while testing:
- showWindow demoted stay_on_top windows (every fullpage/dashboard
app window, created in the 99999999+ z band) to a plain counter
z-index on restore. focusWindow deliberately never raises
stay_on_top windows, so after minimize/restore a single dashboard
focus buried the app permanently. Restore now re-raises into the
stay-on-top band.
- Pressing the drawer now focuses its window, as the titlebar it
replaces did. The document-level activation handler works off
mouseover_window, which only mousemove refreshes — a tap with no
intervening mousemove (touch, restored windows) would re-raise
the dashboard over the app.
* Dashboard: restored windows come back with the drawer collapsed
The restore re-played the open intro (expand + auto-retract). A restore
should bring back the app, not the chrome — the user already met the
controls on open, and they just USED them to minimize. showWindow now
forces the drawer shut instead, which also covers the Back-button
minimize path, where the window hides without touching drawer state.
* Dashboard drawer: left-align the title against the icon
The tray is asymmetric — one icon on the left, two buttons on the
right — so a title centered in its fixed box landed off the tray's
true center and read as misaligned against the centered tongue below.
Left-aligned, icon + title cluster as the identity on the left and
the controls cluster on the right.
* Dashboard drawer: morph the tongue into the tray instead of sliding
The drawer is now ONE glass surface that changes shape, rather than a
tray that slides in from off-screen with a handle below it. At rest
it's the tongue; opened, the same surface swells into the tray while
the grabber bar rides the morph and settles into a slim strip along
the tray's bottom edge as the dismiss handle — same handle, both
states. Content is revealed from the center outward through an inner
clipping layer (the surface itself can't be overflow:hidden without
clipping the toggle's touch halo) and condenses in/out of focus with
an opacity + blur + scale crossfade. Opening gets a soft overshoot
and settles; closing is quicker and lands without bounce.
The open width is DERIVED in CSS from the same part variables that
lay out the controls row, so the surface always fits its content
exactly and breakpoints (touch sizes, hidden title under 500px) only
override parts.
* Refine dashboard drawer open-state handle
Update the headless dashboard app drawer so the grabber bar disappears when the drawer is open and only the bottom dismiss strip remains interactive. This adjusts tray/strip sizing on desktop and mobile, adds opacity timing for smooth dissolve/reappear transitions during open/close, and aligns in-code docs/comments with the new behavior.
* Dashboard: fix minimized apps getting permanently buried under the dashboard
Fullpage/dashboard app windows are created stay_on_top in the
99999999+ z band, but showWindow's restore path demoted them to a
plain counter z-index — and since focusWindow deliberately never
raises stay_on_top windows, the demotion stuck: one dashboard focus
after a minimize/restore buried the app permanently (URL and tab
title still naming an app the user could no longer see). Restore now
re-raises stay-on-top windows into their band.
Also make the control pill focus its window on mousedown, as the
titlebar it replaces did. The document-level activation handler works
off mouseover_window, which only mousemove refreshes — a tap with no
intervening mousemove (touch, restored windows) would activate the
stale window and could re-raise the dashboard over the app. Deferred
a tick so it runs after that handler and wins.
* Dashboard: restored windows come back with the pill collapsed
The restore re-played the open intro (expand + auto-retract). A restore
should bring back the app, not the chrome — the user already met the
controls on open, and they just USED them to minimize. showWindow now
forces the pill shut instead, which also covers the Back-button
minimize path, where the window hides without touching pill state.
* Dashboard pill: left-align the title against the icon
The pill is asymmetric — one icon on the left, two buttons on the
right — so a title centered in its fixed box landed off the pill's
true center and read as misaligned. Left-aligned, icon + title
cluster as the identity on the left and the controls cluster on
the right.
* Dashboard: headless full-tab apps with a floating control pill
Maximized app windows in dashboard mode no longer render a titlebar —
the app covers the full tab. The head's controls move to lighter-weight
affordances:
- A floating pill overlaid top-center of the app (parent DOM, above the
iframe): expanded it shows the app icon, title, minimize, and close;
it opens expanded so first-time and deep-link users see it, then
collapses to a subtle capsule that re-expands on hover, tap, or
keyboard focus. Minimize routes through the same URL-entry consumption
as the Back button; close tears the window down normally. Dark
translucent glass so it reads over any app content; safe-area aware;
bigger targets on coarse pointers; title hidden under 500px; respects
prefers-reduced-motion.
- Apps-tab tiles double as the app switcher: a macOS-dock-style running
dot marks tiles with a live (visible or minimized) window, and the
tile context menu gains a Quit item for running apps. UIWindow
broadcasts window open/close so the dots stay current.
- puter.ui.setWindowTitle also updates the pill title.
Only maximized, URL-owning app windows in dashboard mode go headless:
dialogs, explorer, non-maximized child windows, and everything on the
desktop keep their titlebars. dashboard.css's 29px head-height
compensation is scoped to windows that still have heads, so headless
app bodies fill the window exactly.
* Fix pill buttons swallowed by their own hover halo
The pill's invisible hover halo (an absolutely-positioned ::after with
negative inset) painted above the pill's static children, so real
pointer clicks hit-tested to the halo and never reached the minimize/
close buttons — while element.click() (which skips hit-testing) worked,
hiding the bug from programmatic checks. Stack the halo at z-index -1
(the pill is its own stacking context, so the outside ring still sits
above the app's iframe) and raise the interactive children explicitly.
* Review round: pill/dashboard edge-case fixes
- Latch pop_dashboard_app_url while its history.back() is in flight: a
double-click on minimize (or a close racing a minimize) issued two
back()s, and the second popped the dashboard's own entry — navigating
clean out of the page. Any popstate or push settles the latch.
- Close a running app's windows when it is uninstalled: the tile is a
headless app's only switcher, so a minimized instance would otherwise
be stranded — impossible to restore or quit.
- puter.ui.setWindowTitle in dashboard mode now also updates the
browser-tab title (while that app owns the URL) and the window's
data-name, so a later restore re-applies the app's current title
instead of its launch-time one. Desktop behavior unchanged.
- Nudge collapsed-pill contrast (opacity 0.55 → 0.65, border alpha
0.16 → 0.22) so the capsule stays findable over dark app content.
* Adjust dashboard tile spacing and label sizing
Tweaks My Apps tile layout constants to match the real icon/label geometry: tile height is increased, label top margin is widened, and the running-status dot is moved slightly lower to avoid overlap during icon hover scaling. It also raises dashboard app pill title line-height to prevent descenders from being clipped when ellipsis overflow is active.
* Pill and running-dot visual polish
- Fixed-width pill: the title gets a fixed 150px centered box instead of
a cap, so every app's pill is identical and the minimize/close buttons
sit at the same screen position in every app — they're operated by
muscle memory, like any titlebar's controls.
- Give the pill title line-height 1.35: overflow:hidden (needed for the
ellipsis) also clips vertically, and at line-height 1 it cut the
descenders off g/p/y.
- More air around the tile running dot: label margin 6px → 10px and the
dot 2px lower (tile row height 78px → 82px to keep the label line in
budget — the pager layout reads the CSS var, so it follows). The
icon's 1.08x hover growth no longer touches the dot.
* Size the pill icon to balance the capsule
16px read as undersized next to the 26px buttons in the 36px capsule —
bump the expanded icon to 20px with a proportional (25%) radius, and
object-fit: cover in case an app icon isn't square. Collapsed size is
unchanged.
- Opening an app in dashboard mode pushes a real history entry at
/app/<name>; Back minimizes the app (it keeps running), Forward
restores it — or relaunches it if it was closed
- The minimize/close buttons consume the entry via history.back() so
the address bar never names an app that is no longer on screen
- Landing directly on /app/<name> now boots the dashboard with the app
open maximized in-page instead of the desktop; embeds, popups, and
explicit fullpage boots keep the desktop flow, and fullpage_on_landing
no longer opts a landing out of the dashboard
- Desktop mode no longer rewrites the URL at all (no /app/<name> or
folder-path replaceState on focus, no /desktop resets on close or
minimize); tab titles still track the focused window
Update `showWindow` to use a shorter, window-only morph when restoring a minimized app from a dashboard tile. `morph_window_from_tile` now accepts options for `icon_half` and `duration`, gates icon ghost creation accordingly, and derives transition/cleanup timing from the configured duration so un-minimizing feels distinct from a full app launch.
Update the dashboard app tile click handler so Ctrl/Cmd+click opens apps in a new tab (matching the context menu behavior) while keeping existing target-link/app-name routing. Also switch the default body background to white and add a dark-mode media query to use a dark background color.
Removes `isMobile.phone` checks that previously disabled dashboard tile morph transitions. Window open/close morph selection and tile-launch ghost setup now run on phone the same way they do elsewhere, while still honoring animation-disable and reduced-motion conditions.
Add click-time launch feedback for dashboard app tiles so users get immediate visual response while app startup is still loading. TabApps now starts and settles tile launch state around `launch_app`, logs launch failures, and keeps duplicate-launch protection.
UIWindow now tracks in-flight tile feedback with a WeakMap, introduces `begin_dashboard_tile_launch`/`settle_dashboard_tile_launch`, and updates `morph_window_from_tile` to avoid replaying the icon half when it already ran at click time. Ghost/icon timing and cleanup were adjusted so fallback and failed launches do not leave stale tile state.
A fresh launch has a server round-trip (app info, signing) between the
click and the window's creation, so the open morph used to start late —
on a slow connection the click gave no feedback at all. The two halves
of the morph are now decoupled:
- On click, begin_dashboard_tile_launch starts the icon's half
immediately: the ghost enlarges in place and dissolves exactly as
before, and the real icon stays hidden — the empty slot reads as
'launching' — while the fetches run.
- When the window opens, morph_window_from_tile claims the click-time
ghost instead of spawning a second one, runs the window half alone,
and restores the icon when it lands. On a fast launch the halves
overlap into one motion; on a slow one the window zooms out of the
empty slot.
- settle_dashboard_tile_launch (in the launch's finally) puts the icon
back whenever the window never claims it: launch failed, app opened
in background, or the morph fell back to the plain fade. The launch
also gained a .catch, so a failed launch logs instead of leaking an
unhandled rejection.
The un-minimize path has no fetch and is untouched: it still spawns its
own ghost inside the morph.
* feat(gui): open dashboard apps maximized in-page with minimize + single instance
Apps launched from the dashboard's Apps tab now open as maximized Puter
windows in the same page (via launch_app) instead of a new browser tab.
External website shortcuts still open in a new tab since arbitrary sites
can't reliably be iframed.
Dashboard app windows keep their minimize button (fullpage mode normally
hides it since there's no taskbar). With no taskbar item to animate
toward, hideWindow now hides the window in place and flags it with
data-minimized_in_place; showWindow un-hides it via that flag. An
explicit flag is used because data-orig-* can't distinguish the two
minimize paths - drag/maximize handlers set those attrs too.
Tiles are single-instance: clicking a tile un-hides a minimized
instance or focuses a visible one instead of launching a duplicate, and
launches in flight swallow repeat clicks.
Also fixes .window-body-app height in dashboard mode: fullpage mode
sizes it to 100% assuming no titlebar, which clipped the bottom 29px of
every app.
* feat(gui): iOS-style minimize-to-icon morph in dashboard mode
Minimizing a window in dashboard mode now zooms it into its app's tile
on the Apps tab — but only when the user can see that tile (Apps section
active AND the tile on the pager page currently in view); otherwise the
existing hide-in-place fade is kept, as on phones and with
prefers-reduced-motion.
The animation is a two-layer morph on one 450ms cubic-bezier(0.32,
0.72, 0, 1) path: the live window scales onto the tile's icon box
(transform + opacity only — no per-frame iframe reflow) while its corner
radius grows toward the icon squircle, and an enlarged clone of the icon
flies congruently with it, cross-fading in as the window fades out and
landing exactly on the real icon's slot (hidden until the handoff), so
the window reads as becoming the icon.
Window geometry is never touched: only inline transform/opacity/radius
are set and restored byte-identical when the animation ends, and
showWindow()'s existing minimized-in-place path restores as before —
including a reopen mid-animation.
* fix(gui): keep the minimize-morph ghost icon square (contain-fit)
The icon clone previously mapped rect-onto-rect over the window, which
stretched the square icon anamorphically. A covering square was tried
and jittered: it protruded past the card's short edge and the halo
popped in mid-crossfade.
Contain-fit resolves both: the ghost scales uniformly (always square),
sized to the window's short edge and centered — its side tracks the
card's short edge exactly for the whole flight (both interpolate
linearly between the same endpoints on the same curve), so the card's
rectangular flanks melt away around a steady, undistorted icon.
* feat(gui): add 'Open in new tab' to app tile context menu
Every tile's right-click menu now leads with Open in new tab, which
opens the app the way tiles did before in-page windows: /app/<name> in
a new browser tab (noopener), or the site link for external tiles.
Non-uninstallable apps previously had no menu at all; they now get this
single item, and uninstallable ones show it above a divider + Uninstall.
* feat(gui): reverse morph — dashboard tile icons zoom into their opening window
Opening an app in dashboard mode now runs the minimize morph backwards:
the window grows out of its tile's icon box (transform + opacity only,
radius relaxing from the 22% squircle to the resting value) while a clone
of the icon enlarges IN PLACE on its slot — capped at 2.5x, fully dissolved
by ~250% growth — as the window fades in underneath on the same 450ms
cubic-bezier(0.32, 0.72, 0, 1) path with the crossfade handoff mirrored
(window in at 80-240ms, ghost out at 120-320ms).
Both open paths get the morph, with the same visibility conditions as
minimize (Apps section active, tile on the pager page in view, not phone,
no reduced motion), falling back to the existing fades otherwise:
- fresh launch: TabApps passes window_options.morph_from_dashboard_tile,
and UIWindow morphs instead of the 70ms opening fade. Since .window is
display:none from the stylesheet until later in the pipeline, the hook
shows it before measuring (nothing has painted yet, so neither the early
show nor the hide-back on a declined morph can flash).
- un-minimize: showWindow's minimized-in-place branch morphs instead of
fadeIn(150), keeping the z-index bump and 80ms focus timing.
A data-window_morphing flag guards against opposing morphs fighting over
the window's inline styles: minimize clicked mid-open-zoom (or a tile
click mid-minimize-zoom) falls back to the pre-morph fade behavior, and
each animation still restores every inline property byte-identical when
it ends.
* Update dashboard.css
* feat(gui): cap the open-morph ghost icon at 2x, dissolving in place
The tile icon no longer flies toward the window center nor grows to full
contain-fit (10x+ reads as a giant blurry sticker): it enlarges in place
on its own slot and its growth is capped so the fade-out completes right
around 200% of its size — the window alone carries the motion and the
rest of the growth.
* fix(gui): drop the tile :active press-shrink that stuttered the open morph
Clicking a tile launches the app and the open morph grows the icon in
place — the 0.95 press-shrink made every launch read as shrink-then-grow.
The icon now holds its hover scale through the click and enlarges
straight from there (the morph ghost is measured from the rendered rect,
so it picks up the hovered size seamlessly).
Adds a 'View Users' link on the app Analytics tab that opens a modal
listing users who have authenticated into the app (username + email,
where the user has granted email:read to the app).
Consumes the existing puter.apps.get(...).users()/.getUsers() client
SDK method, backed by the app-telemetry driver's get_users - no new
backend work needed, per discussion on the issue.
Relates to #3349
* Update docs meta tags and description
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
---------
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* feat(email): accept custom message headers in sendRaw
Lets callers set transport-level headers such as List-Unsubscribe /
List-Unsubscribe-Post. Also documents the puter-email driver's automatic
unsubscribe / report-abuse footer and the new `suppressed` result field
in the puter.js email module.
* feat(email): envelope override in sendRaw + suppressed in EmailSendResult type
Adds an optional `envelope` field to SendMailOptions so callers can set
transport recipients independently of the visible To/Cc headers (used by
the email-send driver for per-recipient deliveries), and adds the
`suppressed: string[]` field to the public EmailSendResult typing to
match the driver's documented result.
Co-Authored-By: Claude Fable 5 <[email protected]>
* feat(email): document per-recipient best-effort delivery via `failed`
The email-send driver now attempts every recipient's private delivery
even when one fails, returning failed addresses in the result's
`failed` array instead of failing the whole call — so callers retry
only the failed subset and never re-mail delivered recipients. Adds
the field to the public EmailSendResult typing and module docs.
Reworked the Files view toggle into a context menu with List, Compact Grid (`grid-sm`), and Grid modes, and refactored view handling into shared helpers (`isGridView`, `viewClass`, `setView`, `applyViewMode`) so grid behavior is applied consistently across rendering and drag previews. Added compact-grid CSS overrides to reduce tile, icon, text, and badge sizing while reusing existing grid layout behavior.
Switches the Files tab header navigation buttons to new thin arrow SVG assets and aligns icon styling with a lighter visual weight. It also updates several header action SVG paths (new folder, upload, list/grid, sort, select) and sets path action icon color to #5c5c5c so the controls look consistent.
Apps tab: tile rows only budget one label line, so two-line app names
made flexbox shrink the icon box into a landscape crop and clip the
label mid-glyph. Clamp labels to a single line and exempt the icon box
from flex shrinking.
Files tab: header hover states (nav arrows, breadcrumbs, action
buttons) now use a neutral #EEE chip and no longer recolor the glyph;
nav arrows also render at full opacity when enabled.
Prevent app tile icons from shrinking when labels overflow, and clamp tile labels to a single line so they fit within the fixed tile height. This keeps the grid layout stable while preserving full app names in the existing tooltip.
* fix: don't double-refresh Dashboard tabs on browser back/forward
A single back/forward navigation fires both popstate and hashchange, so
handleRouteChange ran twice, calling the tab's onActivate twice — every
back/forward to the Apps tab issued duplicate /installedApps and
/get-launch-apps requests and re-rendered the tab twice. Track the last
handled URL and skip the second event.
* fix: store raw values in Dashboard file-row data attributes
renderItem passed html_encode()d strings to setAttribute, which stores
them literally — a file named "Tom & Jerry.txt" got
data-path="...Tom & Jerry.txt". Every flow that reads the attribute
back (keyboard cut/copy/paste, delete-to-trash, drag-and-drop, selection
actions) then hit the server with the mangled path and failed with
"Entry not found". The socket item.renamed/item.updated handlers had the
same problem via jQuery .attr(), and item.moved/item.removed matched rows
with html_encode()d selector needles that can't match raw attributes.
Store raw values everywhere (matching the desktop convention and the
raw readers throughout TabFiles), match paths by comparison instead of
selector interpolation, and repair the html_encode(x) ?? fallback
expressions that could never take their right-hand side. Verified live:
copy/paste of a name containing '&' now succeeds.
* fix: escape folder names in Dashboard nav-history menu (stored XSS)
The back/forward taphold menus interpolated path.basename(history_item)
straight into UIContextMenu item html, which renders it verbatim. A
folder named with an HTML/script payload executed when the user opened
the history menu after visiting it. The desktop file manager already
encodes this value; match it with html_encode(). Verified live: the
payload now renders as inert text and its onerror never fires.
* fix: harden Dashboard hash routing against malformed and unknown tabs
Three routing defects, all reachable from a shareable URL:
- A malformed percent-sequence (e.g. `#100%`) made parseDashboardRoute's
decodeURIComponent throw at module load, blanking the entire GUI. Guard
the decode and fall back to the raw hash. Verified: `#100%` now boots.
- A hash whose tab segment contained a quote (`#foo"bar`) was interpolated
into a jQuery selector that throws, leaving dashboard event handlers
unbound. Resolve the route tab against the known tab set (falling back to
Apps) before it ever reaches a selector. This also fixes an unknown hash
activating the Apps section without its `.dashboard-content.apps` styling.
- Re-clicking the already-active sidebar tab pushed duplicate history
entries, so Back became a no-op until pressed repeatedly. Only pushState
when the hash actually changes.
Verified live for all three.
* fix: keep Dashboard file list footer and size cells in sync on live updates
Incremental updates only touched hidden data attributes, so the visible
UI drifted from the data:
- Adding a file via the socket (item.added) or creating one inserted a
row but never called updateFooterStats, so the "N items · size" footer
stayed frozen at the old count. Removals and moves had the same gap.
- item.updated (and the in-place update in UIDashboardFileItem) rewrote
data-size/data-modified but not the on-screen .item-size/.item-modified
cells, so a remote overwrite left a stale size (e.g. "5 B" for a file
that had grown to 2 KB).
Refresh the footer after add/remove/move and repaint the size/modified
cells on update. Verified live: add, overwrite, and delete now all
reflect immediately.
* fix: Dashboard Files shift-click selection after navigation and byte formatting
Shift-click selection:
- renderDirectory cleared the .selected class but left
window.latest_selected_item pointing at a now-detached row from the
previous directory. The shift handler set shift_clicked=true, found the
anchor's index as -1, skipped the range select, and onclick then
early-returned on shift_clicked — so the first shift-click in a new
directory selected nothing. Reset the anchor when it detaches, only take
the range path when the anchor is still in the list, and treat a
shift-click with no valid anchor as a plain select that becomes the
anchor (onclick otherwise ignores clicks while Shift is held).
formatFileSize:
- Returned "NaN undefined" for missing/invalid sizes and "1.5 undefined"
for terabyte-plus files (sizes array stopped at GB). Guard non-finite
and non-positive input, clamp the unit index, and add TB/PB.
Verified live: first shift-click selects the item, a second shift-click
extends the range, and formatFileSize returns 0 B / 1.5 TB / 2 PB.
* fix: show a message instead of a blank pane when a Dashboard folder fails to open
renderDirectory empties the file list before awaiting readdir, so a
readdir rejection (permission lost, folder deleted from another session,
network error) left a completely blank pane with only a console error.
Render a centered "This folder couldn't be opened." message on the catch
path. Verified live: a simulated readdir failure shows the message,
clears the spinner, and leaves navigation working (next folder loads).
* fix: Dashboard Usage/Home tab data freshness, errors, and dead controls
TabUsage:
- The Upgrade/Manage button was shown unconditionally and its click did
`new window.UIUpgradeAccount()`, which only exists on hosted puter.com —
on self-hosted it threw a TypeError from a dead button. Hide the button
when UIUpgradeAccount is absent and guard the click.
- The tab had no onActivate and bound its refresh to a nonexistent
element, so usage numbers were frozen at page-load for the whole
session. Add onActivate to refetch (verified: reactivating refetches).
- Resource names rendered the backend's `_dot_` escaping literally
("gemini-2_dot_5-flash"); un-escape for display and html_encode the cell.
- The two loaders had no error handling; a failed fetch left the tab
blank with an unhandled rejection. Catch each and show a fallback.
- Guard capacity 0 so storage no longer renders "NaN%".
TabHome:
- Plan button kept saying "Manage →" after a subscription lapsed; reset it
to "Upgrade →" in the free branch.
- Guard capacity 0 ("NaN%") on the storage card.
- "Your Plan ›" card header had the arrow affordance but no target and did
nothing on click; point it at the Usage tab like its siblings.
- Returning to the tab fired both focus and visibilitychange, and
refreshAndBroadcast both called refresh() and dispatched the event its
own listener handles — up to 4 duplicate reloads per focus. Drop the
direct call and coalesce the focus/visibility pair.
TabHome + TabApps:
- window.open(externalAppUrl, '_blank') lacked noopener, exposing the
dashboard tab to reverse tabnabbing from an external app site. Add
noopener,noreferrer.
Verified live: Usage and Home render without NaN or console errors, the
upgrade button is hidden on self-hosted, and 'Your Plan' now navigates.
* fix: multiple Dashboard CSS defects (dead rules, contrast, responsive)
- The desktop rule that hides the row ⋮ button ended two selectors with a
comma before an @media block, so the whole construct was invalid and
discarded — the ⋮ showed on every list-view row on desktop instead of
deferring to right-click. Verified: it's now hidden (display:none).
- Native-drop dark-mode styles keyed off .window[data-color-scheme="dark"],
an attribute never set anywhere; converted to @media (prefers-color-scheme:
dark) like every other dark rule so they actually apply.
- .myapps-tile-label set white-space:nowrap after a 2-line -webkit-line-clamp,
defeating the clamp so long app names clipped mid-glyph on one line;
removed it.
- .dashboard-sidebar-separator was defined a second time with a contradicting
box model (background line + full-width margin), rendering a doubled,
edge-to-edge divider; removed the duplicate so the inset border rule stands.
- Grid-view .item-icon had a hardcoded background:white and border-radius:2px
overriding its own border-radius:8px (glaring white tiles in dark mode,
square-ish corners); use var(--dashboard-background) and keep 8px.
- .files-footer used hardcoded #666/#CCC on theme-variable backgrounds
(dim in dark, near-invisible separator in light); use the text/muted vars.
- Fixed the .ui-droppable-over typo (jQuery UI emits ui-droppable-hover).
- The context-menu backdrop's "desktop transparent" rule used min-width:768px
while every mobile rule uses max-width:768px, overlapping at exactly 768px
(a common tablet width) — a non-dimming, invisible modal shield; bumped to
769px.
- At 481-768px the fixed hamburger toggle sat on top of the Files directories
column's first folder because .dashboard-content.files padding wins over the
media-query padding; add top clearance to the directories column there.
Verified light-mode desktop is unchanged (directories padding still 16px, ⋮
hidden, footer colors resolve).
* fix: only offer Uninstall for Dashboard apps where it actually sticks
Uninstall was suppressed via a hardcoded 8-name allowlist that had drifted
out of sync with the backend's ~26 recommended apps. For the ~18 unlisted
recommended apps (Calculator, Code, the games, …) the menu offered
Uninstall, revokeApp resolved, the tile vanished — then get-launch-apps
re-added it on the next load and it reappeared, so the uninstall silently
reverted.
Compute uninstallability from the actual lists: an app is uninstallable
only if it's in the user's installedApps AND not in the recommended list
(and not a protected core app). Recommended apps — installed or not —
resurrect on reload, so Uninstall is hidden for them. Verified live: a
synthetic installed-not-recommended app shows Uninstall; a recommended
app shows none.
* fix: guard Dashboard Apps loads against stale overwrites and drag/error clobbering
loadApps only checked for an in-progress drag before its await, and its
catch wiped whatever was on screen. Three concurrency issues followed:
- A slow, older load resolving after a newer one could overwrite the
newer app list (and clobber a reorder the user saved while the stale
fetch was in flight). Tag each load with an increasing id and skip
applying one only when a strictly newer load has already applied —
gating on "already applied" (not "latest started") so the first load to
resolve still populates the list for the pager's ResizeObserver.
- A drag that began while a load was awaiting could have the grid rebuilt
out from under it; re-check the drag after the await.
- A transient re-fetch error replaced a working grid with "Failed to load
apps"; only show that placeholder when nothing has loaded yet.
Verified live: a slow older load no longer clobbers a newer one, and the
grid still renders normally on activation.
* fix: page through all installed apps in the Dashboard instead of capping at 100
The /installedApps endpoint clamps limit to 100 and paginates, but the
Apps tab fetched a single page — a user with more than 100 installed apps
silently lost the alphabetically-last ones from both the grid and search,
with no way to launch or uninstall them from the dashboard. Loop pages
until a short one comes back (the common <100-app case still makes a
single request). Verified: the request now carries &page=1 and stops
after one page for a small account; the loop pulls all pages otherwise.
* fix: make the mobile Dashboard context menu handle submenus, disabled items, and positioning
The touch context-menu modal (used whenever maxTouchPoints > 0) had three
defects:
- Items with a submenu and no onClick ("New", "Open With") rendered as
plain buttons that did nothing and didn't even close the menu — the two
submenu-bearing actions were simply unreachable on touch. Drill into the
submenu on tap, with a Back row to return.
- disabled items were rendered as active buttons and executed their
onClick, so a folder's disabled "Paste Into Folder"/"Publish as website"
ran anyway. Render them inert (disabled attribute + dimmed class + a
click-handler guard), mirroring the desktop UIContextMenu.
- Non-touch devices that still route here (touchscreen laptops) got the
modal pinned at a hardcoded left:300px, far from the tapped item on a
wide screen. Center it over the target and clamp to the viewport.
Verified live (with maxTouchPoints forced): "Open With" opens its submenu
and Back returns; disabled "Paste Into Folder" is inert; the modal is
positioned near the item.
* fix: repair Apps-tab regressions from the Dashboard bug-fix pass
- A non-array /installedApps response (an error payload) was read as
end-of-pagination, silently rendering the grid without any installed
apps; fail the load so the explicit error state shows instead.
- A loadApps result that resolved mid-drag was discarded with no retry,
freezing the grid on stale data for the rest of the visit; stash it
and apply it after the drag ends, reconciled against the drag's final
order so it can't undo a reorder whose save is still in flight.
- Uninstall disappeared for recommended/recent apps, removing the only
UI path that revokes their permissions; offer it again and instead set
expectations in the confirm modal (the tile stays for apps that
get-launch-apps re-adds).
- init and the initial-route onActivate both fired a full load on open;
share the in-flight load instead of issuing a duplicate request trio.
* fix: repair Files/routing regressions from the Dashboard bug-fix pass
- The no-anchor shift-click fallback also fired when shift-clicking the
current anchor, collapsing an existing multi-selection to one item,
and it ignored Ctrl/Cmd; only run it when there is genuinely no
anchor, and keep the existing selection when Ctrl/Cmd is held.
- Unknown hash values (a stale bookmark, an in-page anchor) were
coerced to 'apps' and yanked the user off their current tab with a
refetch and autofocus; ignore them outright, both at boot and on
hashchange, while still keeping untrusted hashes out of selectors.
- The item.updated socket handler re-implemented the row-refresh from
TabFiles and had already drifted (trashed items showed their raw UID
instead of metadata.original_name); extract a shared row updater and
delegate to it.
* fix: repair Usage-tab regressions from the Dashboard bug-fix pass
- Hiding the plan button whenever window.UIUpgradeAccount was missing
at check time could hide it for the whole session on hosted
deployments that attach the script after dashboard init; keep
re-checking for a while before concluding the install is self-hosted.
- init and the initial-route onActivate both refreshed usage on a
direct #usage open; share the in-flight refresh instead of issuing
duplicate request pairs.
* fix: repair Dashboard CSS regressions from the bug-fix pass
- Repairing the malformed item-more rule put display:none into effect
for the first time, removing the visible-and-used desktop row '⋯'
button; drop the rule instead (it was never in effect on any release).
- The desktop context-menu backdrop breakpoint (min-width: 769px) left
fractional viewport widths between 768px and 769px with the mobile
dimming; use the exact complement of the mobile max-width: 768px
rules instead.
* fix: make Dashboard app uninstall honest about recents and resilient mid-flight
- Recently-opened apps were classified as sticky-removable, but the
recent list is built from app-open history that a revoke doesn't
touch, so their tiles reappeared on the next load — the very bug the
uninstall gating set out to fix. Treat recents like recommended.
- The confirm modal claimed every staying tile was 'provided by Puter',
which is false for third-party recents; describe the actual reason.
- Uninstall now marks in-flight loads stale so a fetch that started
before the revoke can't resurrect the removed tile.
- A pagination failure after the first page no longer discards the
pages already fetched — one flaky request among N used to turn the
whole grid into 'Failed to load apps'.
* fix: keep the Dashboard Usage tab stable through transient failures
- A failed refresh no longer wipes an already-rendered usage table;
the unavailable note only shows when there is nothing on screen yet
(mirrors the Apps grid's error handling).
- The plan-button check now re-checks indefinitely with backoff instead
of giving up after ~10s, so an arbitrarily late UIUpgradeAccount
attachment can't leave a subscriber without the button all session.
* fix: Dashboard Files footer sync and Home refresh resilience
- The shared row updater now refreshes the footer's item-count/total
line, which is computed from the data-size attributes it just wrote;
a remote overwrite no longer leaves the row and footer disagreeing.
- Size/Modified are only written when the update payload carries them,
so a minimal event can't zero out correct values on screen.
- A failed readdir now resets the footer instead of keeping the
previous directory's counts over an empty pane.
- Home's focus-refresh caps its wait on refresh_user_data, so a whoami
that never settles can't leave the plan/usage cards stale forever.
* fix: contain partial Apps loads and keep the uninstall flag current
- A partial installed-apps list (a page beyond the first failed) now
renders only when the grid is empty — it must never replace a
complete grid already on screen — and saveOrder refuses to persist
while the list is partial, since overwriting the saved order with a
truncated one would drop the missing apps' positions for good.
- Opening an app from the grid now flips its tile's uninstallable flag:
the open puts it in the server-side recent list, so a revoke from
that moment on leaves the tile in place, and the modal must not act
on the stale load-time snapshot.
- The in-flight-load invalidation moved into _invalidateInFlightLoads()
so the seq protocol lives in one place.
* fix: Usage-tab error guard, dead spinner, and poll decay
- The transient-error guard now tracks 'a table has rendered' instead
of data length, so an account with legitimately zero usage doesn't
get its empty table replaced by the unavailable note on a flaky
refresh.
- update_usage_details animated a spinner element that exists nowhere
in this tab and held every refresh open for an artificial 1s minimum;
both halves of the dead control are now gone.
- The plan-button recheck backs off to a 60s heartbeat instead of
polling at 2s forever on installs where UIUpgradeAccount never
appears.
* fix: guard minimal file updates; tighten selection and socket hot paths
- The shared row updater only writes the fields an update payload
actually carries, so a minimal event can't blank a row's name/path —
the same hazard the size/modified guards already covered.
- The all-rows lookup in the selection handler now happens only under
Shift instead of on every click, and the single-select block shared
by the no-anchor shift-click and drag-handle paths lives in one
selectSingle helper.
- item.removed and item.moved narrow by data-uid (O(1) selector) before
comparing paths instead of scanning every row per socket event.
* fix: replace partial-load and uninstall-flag patches with convergent designs
The previous round's containment patches each spawned new corner cases
(a persistently failing page froze all refreshes; the partial-load
saveOrder bail silently discarded reorders; the tile-open flag flip
lost races with in-flight loads, server recording, and popup blockers).
Replace both mechanisms:
- A partial installed-apps fetch (later page failed) now always applies,
supplemented with the apps already known from the previous list — the
grid and search can't shrink from one flaky request, refreshes never
freeze, and an empty grid still gets the fetched pages.
- saveOrder carries over saved names missing from the current list
instead of refusing to persist, so no app's saved position is ever
dropped; stale names are harmless (reconcileAppOrder ignores them)
and preserve the position of apps that return.
- The load-time uninstallable snapshot is no longer patched client-side
on tile open. Uninstall now revokes, optimistically removes the tile
when expected to stick, and refetches — the grid converges to
server-side truth instead of guessing at recents state.
* fix: don't blank filenames on metadata-only updates; unify range selection
- A payload carrying metadata but no name resolved to an empty display
name and blanked the row's visible filename — skip the write when the
resolved name is empty.
- The shift-range branch now goes through the same applySelection
helper as the no-anchor and drag-handle paths (it takes the row set),
removing the last hand-rolled copy of the selection bookkeeping.
* fix: rank-preserving saved-order merge; keep the pager put after uninstall
- The saved-order carryover appended missing apps' names at the tail,
permanently demoting their saved grid positions — the very thing it
claimed to protect. Replace it with mergeSavedOrder in appOrder.js
(beside its tested siblings, with unit tests): each missing name
keeps its rank among surviving names, so a drag during a partial-load
session neither drops hidden apps' positions nor teleports them
behind the dragged tile.
- The post-uninstall convergence refetch rendered without preservePage,
snapping the pager back to page 1 and replaying the load fade; thread
render options through loadApps so the background sync keeps the
user's page.
* fix: keep hidden apps' saved ranks through the mid-drag stash path
- _applyPendingLoad reconciled a stashed load against the visible-only
on-screen order, tail-appending any app returning to the grid and
contradicting the order the drag had just saved — the rank-demotion
defect resurfacing through one more path. It now prefers
_savedOrderNames, the canonical saved list that saveOrder keeps
merged with hidden apps at their ranks, over the possibly pre-drag kv
snapshot the stashed load fetched.
- The post-uninstall optimistic render now also skips the load fade;
it was the one in-place rebuild still hiding the grid behind the
opacity-0 icon gate.
- mergeSavedOrder's rank rescan collapsed to a single forward
insertion pointer (verified output-identical; the 20 unit tests pin
the behavior).
* fix: gate stale kv-order snapshots on the latest local save
A drag that started and ended while a load was in flight escaped the
stash path entirely: the load applied normally and replayed its
pre-drag kv snapshot, visibly reverting the just-saved reorder (and
permanently clobbering it after the next save). Conversely, the stash
path unconditionally preferred the local record, discarding a genuinely
fresher order fetched from another window when the drag committed
nothing.
Record the load-seq boundary at each saveOrder; a resolving load (both
apply paths, via _resolveOrderNames) replays its fetched kv order only
when it was issued after the latest local save — otherwise the
canonical in-memory saved list wins. Also rewrites the _applyPendingLoad
header, which described the pre-rank-merge implementation.
* Simplify app uninstall tile handling
Remove the uninstallability flag and modal warning logic, and treat uninstall as a local UI removal after permission revoke. This avoids an immediate reload that could re-add recommended/recent apps, while stale in-flight loads are still invalidated. Also simplifies app loading by dropping unused render options from `loadApps`/`_fetchAndRenderApps` and related merge comments.
* fix: match raw item paths in move_items cleanup and shortcut re-point
Item rows store data-path/data-shortcut_to_path unencoded, so the
html_encode()d attribute selectors missed names containing & < > " '
and the destination-row exclusion guard could remove a legitimate row
just created by a concurrent item.moved handler. Compare raw values
case-insensitively instead.
* fix: bound the Usage plan-button retry poll
The UIUpgradeAccount retry chain decayed to a 60s heartbeat but never
stopped, leaving self-hosted installs polling forever and pinning the
captured $el_window. Give up after ~30s of decaying retries; onActivate
re-checks on every return to the tab, so late script loads still get
the button.
* fix: drop the recent-opens list from the Apps tab grid
Recents are open history, not installs: they resurrected uninstalled
apps' tiles and showed merely-visited sites as if installed. The grid
is now recommended + installedApps; anything the user actually uses
still appears because opening an app grants it a permission. Recents
still power the Home tab.
On iOS/WebKit the pager's `touch-action: pan-x` made horizontal touchmove
non-cancelable, so the moment a long-press drag moved the finger the
browser started a native page-pan and fired pointercancel, aborting the
reorder before it visibly began. Preventing pointermove doesn't stop
native scrolling on iOS, and the reordering `touch-action: none` rule was
applied only after the drag began — too late, since touch-action is
latched at gesture start.
Drop the explicit pan-x (back to auto) so horizontal touchmove is
cancelable, and install a non-passive touchmove listener that
preventDefaults only once reordering is armed (post long-press). Before
arming it's a no-op, so a quick swipe still flips pages natively.
* feat(dashboard): drag-to-reorder apps in the Apps tab
Let users arrange their My Apps tiles in any order via drag-and-drop.
The order is stored per-user in puter.kv (`dashboard_apps_order`) as a
list of app names and re-applied on load; apps installed since the order
was saved are appended, and uninstalled ones are dropped.
Interaction:
- Mouse/pen: drag past a small threshold to reorder; a plain click still
opens the app.
- Touch: long-press to pick up a tile (a quick swipe still flips pages).
- Dragging to a scroller edge dwells, then flips to the adjacent page so
tiles can move across pages; the floating drag image lives on <body> so
the pager's overflow can't clip it, and displaced tiles reflow with a
FLIP animation.
- Escape (or losing the window) cancels and reverts; reordering is
disabled while a search filter is active; prefers-reduced-motion is
honored.
Also fixes a latent pager bug where programmatic page changes (arrows,
dots, wheel) didn't update the active page indicator, because those
scrolls don't reliably emit 'scroll' events; goToPage now tracks the
page eagerly.
The order-reconciliation logic is extracted into a pure module
(appOrder.js) with unit tests.
* fix(dashboard): ignore stray pointers and no-op drags
- Filter drag pointermove/up/cancel by pointerId so a second finger can't
hijack or prematurely end an in-progress reorder.
- Only persist a new order when the drop actually changed it, so an
accidental long-press or drop-in-place doesn't freeze the default
ordering into a custom one.
* fix(dashboard): address review of app drag-to-reorder
Correctness:
- Preserve the touch long-press → Uninstall path. A long-press now *arms*
reordering instead of immediately grabbing the tile; moving begins the
drag, while holding still lets the native context menu fire (which
cancels the pending pickup). This restores the only touch route to
Uninstall that the first cut had removed.
- Only swallow the post-drag click when the drop actually reordered
something, so a small pointer drift or a drop-in-place still opens the
app like a plain click.
- Guard the pre-start "intent" window: cancel a pending pickup before any
re-render (ResizeObserver / loadApps), and bail out of _beginDrag if the
pressed tile was detached, so a layout change mid-press can't reinsert a
stale node and persist a corrupted order.
Cleanup:
- Reuse reconcileAppOrder for the drop path instead of a duplicate helper.
- Read the saved order in parallel with the app-list fetches.
- Cache the reduced-motion MediaQueryList instead of re-querying per move.
- Fold the touch-scroll cancel into _endDrag(false); drop _abortDragIntent.
* feat(dashboard): smoother, more tolerant app reordering
Make drag-to-reorder feel like the iOS home screen instead of snapping
and flickering:
- Slow the reflow to 320ms with a gentle spring easing.
- Hit-test against each tile's *resting* box, not its live
getBoundingClientRect. Mid-animation a tile sits between slots, so
testing its live box was dropping it under the pointer and swapping it
straight back — the source of the back-and-forth jitter.
- Require the dragged icon's centre to be well inside a tile before it
becomes the drop target (a deadzone around each tile), so hovering a
boundary no longer thrashes.
- Probe with the dragged icon's centre rather than the fingertip, and
batch the FLIP into a single reflow so large grids stay smooth.
* redesign: compact, scannable Dashboard Session Manager
The session list rendered each entry as a bulky card with a 5-row
Created/Last active/Expires/IP/Client key-value table and a large filled-red
Revoke button, so only a couple of sessions fit on screen and destructive
actions dominated the layout.
Reworked it into an icon-led, compact list:
- One row per session: a device/kind icon tile (laptop / phone / globe by OS,
bolt for workers, key for API tokens, the real app icon for app sessions),
the title + badges, and two tight meta lines (client · IP, then a fainter
last-active · created · expires with absolute-time tooltips).
- Destructive actions are calm: per-row Revoke and the rename pencil are quiet
and reveal on hover (always shown on touch); "Revoke all other sessions" is a
subtle ghost button instead of a filled block competing with search.
- Current session gets a green accent, tint, and pill, and has no revoke button.
- Toolbar search gains an inline icon + focus ring; added a session count line;
restyled kind badges; nested child sessions get a proper tree line.
Styling uses the existing dashboard design tokens. All behavior is preserved:
search filtering, parent/child tree with expand/collapse, inline rename
(optimistic + rollback), per-session revoke (access-token vs session routing),
revoke-all, current-session guard, and focus/interval refresh.
Adds ui_session_count_one / ui_session_count_other to en.js (other locales
fall back to en).
* refactor: render Session Manager as a responsive modal, not a UIWindow
Replaces the draggable UIWindow shell with a self-contained DOM modal:
- Backdrop + centered card on desktop (max 680px / 90vh); full-screen sheet
on phones (<=640px). Fade/scale in, respects prefers-reduced-motion.
- Adds a header bar (title + close), backdrop-click and Escape to close, and
a scroll-contained body.
- Confirmations no longer use UIAlert (itself a UIWindow). Revoke, revoke-all,
and the rename-error path now use in-modal confirm/alert sheets, so there is
no cross-window z-index juggling — important because this can be opened from
a stay_on_top window (UIWindowCopyToken).
Drops the UIWindow and UIAlert imports entirely. All behavior is preserved:
search, parent/child tree + expand/collapse, inline rename (optimistic +
rollback), per-session revoke (access-token vs session routing), revoke-all,
current-session guard, and the focus/60s-interval refresh (cleaned up on close).
* fix: prevent crash rendering website files without a workers array in Dashboard Files
TabFiles.renderItem accessed `file.workers.length` unguarded when deciding
whether to show the website badge. Directory entries that have a published
website but no `workers` array (the common case) made this throw
`Cannot read properties of undefined (reading 'length')`, so the row failed
to render — the file silently disappeared from the list (swallowed by the
Promise.allSettled batch) or was never added on socket-driven updates.
The sibling `is_worker` derivation on the same entry already guards with
`file.workers?.length`, and the desktop reference (UIItem.js) normalizes
`workers` to an array and shows the badge via `!is_worker`. Reuse the safe
`is_worker` flag here instead of touching `file.workers.length` directly.
* fix: correct Dashboard usage that showed negative spend for users with credits
The Usage tab and Home usage card derived month-to-date spend as
`monthUsageAllowance - allowanceInfo.remaining`. But the metering service
computes `remaining = remainingAllowance + remainingPurchasedCredits`
(MeteringService.getAllowedUsage), so `remaining` exceeds the monthly
allowance whenever a user holds purchased credits — making the displayed
spend and percentage negative (e.g. "-$9.90 used of $0.25", "-3960%").
Use the actual reported spend (`res.usage.total`, already returned by the
same endpoint) and clamp the percentage/bar to 100%. For a free user with no
credits and no overage this yields the identical value as before.
* fix: make Dashboard Files Download button work for a single selection
In mobile select mode the floating action bar appears with one item selected
(minCountForActionBar is 1) and shows an enabled Download button, but the
click handler only acted when 2+ rows were selected — so downloading a single
file did nothing.
Gate on `> 0` instead. window.zipItems already normalizes a single item to an
array, so the one-file case works. On desktop the bar only appears at 2+
selections, so this changes nothing there.
* fix: auto-fit column width in Dashboard Files no longer scales with row count
Double-clicking a column resize handle is meant to fit the column to its
longest cell. The per-row loop did `maxWidth = Math.max(maxWidth + 10, textWidth)`,
which forces `maxWidth` to grow by at least 10px on every row regardless of
content — so a folder with N files produced a column ~10*N px wide and
persisted that runaway width to KV storage.
Compare against the running max only (`Math.max(maxWidth, textWidth)`);
`textWidth` already includes cell padding.
* fix: clear empty-directory placeholder when a file is added incrementally
When a directory is empty, renderDirectory appends an absolutely-positioned
"No files in this directory." placeholder. If a file then appears via an
incremental path (socket item.added/item.moved -> UIDashboardFileItem), the
new row was inserted but the placeholder was never removed, so the "No files"
text overlapped the real item. insertAtSortedPosition can't clear it because
the placeholder has no `.item.row` class.
Remove the placeholder in UIDashboardFileItem before inserting the row,
matching what the in-app New Folder / New File flows already do.
* fix: stop leaking a document keydown handler per uninstall-modal dismissal
showUninstallModal bound `keydown.uninstall-modal` on document but only
detached it on the Escape and Confirm paths. Dismissing via Cancel or a
backdrop click called `close()`, which just removed the overlay and left the
handler attached (closing over a now-detached overlay). Each open→cancel
cycle stacked another document-level listener for the page lifetime.
Move the `.off('keydown.uninstall-modal')` into `close()` so every dismissal
path cleans up, and drop the now-redundant explicit detaches.
* fix: resolve 2FA setup promise on every close so Dashboard toggle can't freeze
UIWindow2FASetup only resolved its promise from the Done button and, in
on_before_exit, when setup had NOT succeeded. Clicking "Enable" sets
setup_succeeded=true and advances to the recovery-codes screen without
resolving; if the user then closes that screen via the backdrop or Escape
instead of Done, the promise never settles.
The Dashboard Security tab is the only caller: it disables the 2FA toggle and
`await`s this promise, so the hang leaves the toggle permanently disabled and
stuck in the pending state for the rest of the session. Resolve with
setup_succeeded on any exit; resolve_promise is idempotent so a prior
Done-resolve is unaffected.
* fix: refresh Dashboard tabs when navigating via browser back/forward
The sidebar-click and initial-route handlers both call the target tab's
onActivate (its refresh hook), but handleRouteChange — wired to hashchange
and popstate — only swapped the active classes. Since tab switches use
history.pushState (which doesn't fire popstate/hashchange), the only way to
reach handleRouteChange is browser back/forward, and doing so left tabs like
Home/Apps showing stale data.
Look up the tab by id and call onActivate after switching, matching the
sidebar-click path.
* fix: refresh Home "Recently used" apps instead of freezing them for the session
loadRecentApps only hit /get-launch-apps when window.launch_apps.recent was
empty. Once populated (typically at page load), every subsequent init/onActivate
/focus just re-rendered the same cached array, so apps launched during the
session never showed up under "Recently used" until a hard reload.
Drop the empty-list guard so the list refetches on the same triggers that
already refresh the usage cards. The fetch can only make the list fresher.
* fix: sync window.user.otp when enabling 2FA from the Dashboard
The disable path clears window.user.otp (in UIWindowDisable2FA), but the
enable path updated only the card's DOM, never the in-memory flag. TabSecurity
html() reads user.otp to decide the toggle/badge state, so after enabling 2FA
a re-render of the Security tab from the cached window.user showed it as
disabled for an account that actually had 2FA on.
Set window.user.otp = enabling on success so both directions stay consistent.
renderItem() showed the shortcut badge whenever file.is_shortcut !== 0.
Directory listings return is_shortcut as 0, but the puter.fs.upload()
result used to incrementally render newly-created items omits the field,
so undefined !== 0 was true and every new file got a phantom shortcut
badge. Normalize is_shortcut to 0/1 before using it.
* Restore Files tab in dashboard
Reinstates the Files tab (TabFiles) that was removed in 973d046d, bringing
back the dashboard file browser: directory navigation, list/grid views,
sorting, drag-and-drop, context menus, upload, and trash support.
- Restore src/gui/src/UI/Dashboard/TabFiles.js (identical to pre-removal)
- Re-wire UIDashboard.js: tab registration, initial file path, item socket
handlers, and route-based file navigation
- Re-add file-tab CSS in dashboard.css and the selection-area rule in style.css
- Re-add dashboard-mode item creation hooks in helpers.js move_items
- Re-add #files/<path> deep-link parsing in initgui.js parseDashboardRoute,
while preserving Apps as the default dashboard tab
* Only remove stale rows when moving items
* Fix Dashboard Files tab bugs
- Escape filenames in renderItem to prevent stored XSS via HTML in file
names (matches UIItem); raw name is still preserved for display/rename.
- Read clone data-id from the nested .row in drag drop handlers so
multi-select drag moves all selected items, not just the grabbed one.
- Resolve sidebar drop target from the element's data-path so dragging
onto Public/Home works (they aren't keyed in user.directories); fix the
matching UID-vs-path comparison in the drag-out handler.
- Set data-is_worker/data-worker_url from is_worker itself instead of an
always-true "!== undefined" check, so worker context-menu behavior only
applies to actual workers.
- Give delete-confirmation buttons explicit values so permanent delete
works in non-English locales.
- Refresh the view after a context-menu move-paste, mirroring Ctrl+V.
- Guard the item.updated socket handler against the client's own echo,
matching the other handlers and UIDesktop.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Prevent Files tab lockup when readdir fails
renderDirectory sets renderingDirectory=true then awaits puter.fs.readdir
with no error handling. readdir rejects on any backend error (permission,
deleted directory, network), leaving renderingDirectory stuck true so the
re-entry guard blocks all future navigation and the spinner never clears.
Catch the rejection and reset state.
* Isolate render failures so one bad entry can't lock the Files tab
renderItem parsed file.metadata with an unguarded JSON.parse, but metadata
is client-writable and stored verbatim, so '', undefined, or malformed
values throw (item_icon.js guards the identical call). Combined with
Promise.all, one bad entry rejected the whole render and left the tab
frozen. Guard the parse and use Promise.allSettled for per-item isolation.
* Clear selection when clicking empty area in Files tab
The empty-area click handler is an arrow function, so `this` was the
TabFiles object rather than the container element, making `e.target === this`
always false. Only clicks landing exactly on `.files` deselected. Use
e.currentTarget so clicks on the container background deselect as intended.
* Avoid stacking image-preview document click handlers
showImagePreview bound a click.imagepreview handler without removing a
prior one. It is re-invoked during image arrow-navigation while a preview
is open, so identical document handlers accumulated until close. Remove
any existing handler before binding.
* Sort live-inserted items by display name
insertAtSortedPosition compared the new item's raw file.name against
existing rows' data-name (the display name). For trashed items, name is
the UID while the real name is in metadata.original_name, so newly trashed
items landed in the wrong position in Trash until a full re-render. Compare
on the new row's data-name to match sortFiles and the existing rows.
* Use display name in in-place item update
UIDashboardFileItem's in-place update path set data-name and the shown
name from raw file.name, unlike renderItem which uses
metadata.original_name || file.name. An update to an already-visible
trashed item replaced its name with the UID and corrupted data-name
(which sort and type-to-search read). Compute the display name to match.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
* test: tests for puter.js
* fix: ship lockfile for coverage devDeps; tolerate missing base coverage
npm ci failed on CI because package.json gained the babel/istanbul
devDependencies without the matching package-lock.json update. Also make
the coverage workflow's base leg best-effort so a base ref that predates
the coverage script reports without the comparison column instead of
failing the run.
Co-Authored-By: Claude Fable 5 <[email protected]>
---------
Co-authored-by: Claude Fable 5 <[email protected]>
* typeify subdomains wip
* initial (untested) logic for LocalWorkerService
* Make it work, add lifecycle expiry since workers are process heavy in current implementation
* fix type errors
---------
Co-authored-by: Daniel Salazar <[email protected]>
Show a subtle centered loading spinner for the Apps dashboard when fetching apps if the request is slow. Implements a 1.5s delay before showing the spinner and guarantees a minimum 1.5s visible time to avoid blinking; the spinner is only shown when the container is empty (so background refreshes keep existing tiles). Adds a finish helper to clear the spinner timer and await the minimum visible time before rendering results or errors. Also adds CSS for the spinner element and its animations.
Move logic that determines a tile's displayed title and target link into resolveTileDisplay (use hostname/index_url for external apps) and have buildTileHtml consume it. Expose data-target-link on tiles. Update app search to match the displayed title, raw title, name, and uid so queries find website-shortcut tiles and external apps consistently (matching Home tab behavior).
* update user pays model docs
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
---------
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Add a negative top margin and matching top padding to the horizontal scroller to prevent first-row icons from being clipped when they scale on hover. Overflow-x:auto previously caused vertical clipping of scaled icons; margin-top:-8px and padding-top:8px bleed the clip edge upward so scaled icons remain visible without changing scroll behavior.
Previously the dashboard used a clean root URL for the 'apps' tab and hashes for other tabs. This change makes tab links and history entries consistently use a hash (e.g. #apps, #home) by always generating href as `#<id>` and pushing `#<section>` to history. Simplifies URL handling and ensures the Apps tab also reflects its hash in the address bar.
* feat: add custom web link icon support and context menu option
Introduce the ability to change and display custom icons for .weblink files. Added a "Change Icon" context menu item that invokes the changeWeblinkIcon helper, and updated the icon rendering logic to use getWeblinkIcon instead of the default link icon. This allows users to personalize web link appearance in the file manager.
* Fix security and robustness issues in web link custom icons
# UIWindowSearch and require the icon data URL body to be pure base64 in
#isValidWeblinkIcon. A shared/downloaded .weblink is untrusted input and
# the previous prefix-only check let a crafted icon break out of the
# <img src> attribute.
# byte-signature MIME sniffing and capping stored icon size (was storing
# full-resolution images as base64).
# without picking a file.
# weblinks no longer fetches every file's contents on each render.
# path instead of the target weblink).
# label, add the AGPL license header, and drop redundant stored icon
# copies.
* Keep SVG weblink icons as vectors instead of rasterizing
Rasterizing a picked SVG to a 256px PNG threw away its scalability for no
real benefit: an SVG rendered in <img> runs in script-free static mode, and
the base64-body validation already prevents attribute injection. Store SVGs
as data:image/svg+xml;base64 (content-sniffed, size-capped), and keep
rasterizing raster formats and oversized SVGs to bound stored size.
Co-Authored-By: Claude Fable 5 <[email protected]>
---------
Co-authored-by: Daniel Salazar <[email protected]>
Co-authored-by: Claude Fable 5 <[email protected]>
The global ctrl/cmd+f shortcut opened UIWindowSearch and preventDefault'd
the keystroke, blocking the browser's native find. Skip that interception
while a dashboard window is open so ctrl/cmd+f falls through to the browser.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Treat the Apps tab as the default (root URL) instead of Home. UIDashboard now reads an initial route from window.dashboard_initial_route?.tab and falls back to 'apps' for unknown/absent routes. Sidebar and section active class logic and tab href/hash generation were updated so 'apps' uses the clean root URL while all other tabs (including home) use #tab. The history.pushState logic was adjusted accordingly. parseDashboardRoute in initgui.js was updated to default to 'apps' and its comment clarified the new behavior.
Update .myapps-tile-icon in src/gui/src/css/dashboard.css to change width and height from 60px to 56px, adjusting tile icon sizing for improved layout and spacing.
Call focusSearch when the Apps dashboard is activated and add a focusSearch helper. The helper imports isTouchPrimaryDevice and skips focusing on touch-primary devices to avoid showing the on-screen keyboard. It retries up to 12 times (30ms interval) until the search input is visible to handle cases where the dashboard is briefly hidden while entering full-page mode.
Update .myapps-tile-icon width and height from 52px to 60px to improve icon visibility and better match the updated UI design. Other styles (overflow, background, transitions) are left unchanged.
When an app is external and provides an index_url, use its hostname as the tile title and store the index_url in a new data-target-link attribute. Update the click handler to open the target link in a new tab for external apps, and fall back to opening the internal /app/{name} page for others. This aligns Apps tab behavior with the Home tab and ensures external apps navigate to their website.
* Mark external apps and use the flag for dashboard titles
An app with no owner_user_id (null/empty) isn't owned by a Puter user —
it's an external, origin-bootstrapped app whose uuid/name/title are all the
opaque app-… id. The dashboard used to detect these client-side by comparing
uuid/name/title and a name.startsWith('app-') check.
Expose an authoritative `external` flag from the API instead:
- /installedApps and /get-launch-apps now return `external`, derived from
owner_user_id, and no longer leak the raw owner id.
- The dashboard (Home + Apps tabs) shows the index_url hostname for external
apps based on `external` rather than the uuid/name/title heuristic.
Add/extend extension tests to cover the `external` flag and non-leak.
* Use hostname for opaque external app titles
Detect opaque external app IDs (when name === title === uid/uuid) and replace the displayed title with the hostname from index_url. Adds a uid/uuid fallback and tightens the external-app check in both the Apps and Home dashboard tabs so they behave consistently; preserves target_link for Home entries.
* Add external flag for apps without owner
Expose an `external` property in AppSummary (toAppSummary in RecommendedAppsService.ts) to mark apps that are not owned by a Puter user. The flag is set when `owner_user_id` is null or an empty string, allowing clients to identify origin-bootstrapped/external apps.
The dashboard derives a hostname title for anonymous (app-…) apps whose
uuid/name/title are all identical, falling back to the host of index_url.
That worked for launch/recent apps but not for apps that come only from
/installedApps, because the endpoint didn't return index_url.
Add apps.index_url to the SELECT (alongside apps.icon, relying on the
functional dependency on the apps.id primary key) and assert it in the
extension test.
Detect anonymous apps whose name/title/uid are identical opaque ids (starting with 'app-') and, when index_url is available, replace the displayed title with the index_url hostname (matching the Home tab behavior). Also persist index_url into the mapped app object and make title a mutable variable to support the change.
* feat: make the dashboard the default interface at root
The dashboard now loads at / (with /dashboard kept as an alias), and the
desktop moves to a new /desktop route. An "Open Desktop" button pinned at
the bottom of the dashboard sidebar switches to the desktop.
Root URLs that carry a desktop-only flow keep booting the desktop:
auth popups (?embedded_in_popup=), app deep links (?app=), direct
downloads (?download=), fullpage mode (?puter.fullpage=), and iframe
embeds.
Desktop-context redirects are retargeted so desktop users stay on the
desktop: logout, the QR "continue on phone" link, the auth cover-page
logo click, and window close/hide URL resets now use /desktop. OIDC
login/signup gains a strictly whitelisted return_to param (/desktop or
/dashboard) so social login returns to the interface it started from.
Also: the dashboard strips ?ref= like the desktop does, ?auth_token=
cleanup preserves the path and tab hash, the logged-out session list
gets the wallpaper back, ?download= also works at /desktop, and the GUI
dev server serves /desktop and /dashboard.
* Add Open Desktop bento card and CSS
Replace the previous desktop-switch UI with a full-width "Open Desktop" bento card that links to /desktop. Removes the pinned sidebar Open Desktop button and the old desktop-switch button/handler, and adds new CSS variables and styles for the desktop card (icon background, shadow, surface/header colors), hover shadow and arrow transition, and layout/padding adjustments.
Changed files: TabHome.js (removed old switch, added anchor card), UIDashboard.js (removed sidebar button), dashboard.css (new desktop color vars and styles; removed legacy desktop-switch/button styles).
* Remove box-shadow from myapps search input
Delete the box-shadow declarations from input.myapps-search::placeholder in dashboard.css. This simplifies the placeholder styling (removes subtle and light shadow layers), likely to reduce visual noise or fix rendering inconsistency.
* Show index_url hostname for opaque app IDs
Only replace an app's title with the hostname from index_url when the app appears to use an opaque id. Add a check for names starting with 'app-' so the hostname is used only if title, name and uuid are identical and the name looks like an opaque app id. This avoids overwriting meaningful user-facing names that may coincidentally match the uuid and clarifies the intent in the comment.
* Update dashboard.css
Include 'contacts' in the RECOMMENDED_APP_NAMES array so the Contacts app appears in the recommended apps list. Updated src/backend/services/apps/RecommendedAppsService.ts.
* fix(docs): highlight all code blocks after client-side navigation
The docs site highlights code on full page load via an inline
hljs.highlightAll(), which targets the 'pre code' selector and so
highlights every fenced block, including those without a language tag.
The client-side navigation path (pathchange handler) instead selected
only code[class^="language"], so fenced blocks written without a
language hint were left unhighlighted after in-app navigation while
appearing correctly on a hard reload.
Align the pathchange handler with highlightAll() by selecting 'pre code'
and skipping blocks hljs has already processed (data-highlighted=yes),
keeping the pass idempotent across repeated pathchange events.
Fixes#3287
* fix(docs): hoist hljs.configure and scope highlight to docs content
Address review nits on the pathchange highlight pass:
- Move hljs.configure({ ignoreUnescapedHTML: true }) out of the per-element
loop since it is global config and only needs to run once.
- Scope the selector to '.docs-content pre code' so the re-highlight pass
only touches the swapped-in docs body and leaves code blocks elsewhere
on the page untouched.
Adjust RECOMMENDED_APP_NAMES ordering: place 'editor', 'camera', and 'recorder' immediately after 'builder' and move 'app-center' accordingly. Also add 'vault' to the recommended apps list. This changes the resolved ordering of recommended apps at call time.
* contextmenu design tweak
* icon gap fix (mobile context menu web component)
* fix submenu item padding
* add dark theme support for puter.ui.contextMenu and puter.ui.setMenubar (works only with puter.env === 'web')
* Add theme test & refactor context menu items
Extract shared mediaMenuItems to remove duplication and simplify context menu calls. Add a light-theme context menu handler and a "Run theme test" button that executes runContextMenuThemeTest(), which programmatically renders menus to verify the theme attribute and .puter-theme-dark class for dark, light, and default cases (mirrors existing Playwright assertions). Update testContextMenu and testContextMenuDark to use the new item factory and clean up rendered menus after checks. UI text is updated to show pass/fail results.
---------
Co-authored-by: jelveh <[email protected]>
Include 'builder' in the RECOMMENDED_APP_NAMES array in src/backend/services/apps/RecommendedAppsService.ts so the RecommendedAppsService treats the builder app as a recommended application and surfaces it accordingly.
* Alpha: compaction support for OpenAI and Anthropic
* update lock
* fix billing for anthropic compactions
* Fix max_tokens bug in together provider
* Fix responses compaction
* feat: meter Gemini thinking tokens and grounding requests
- Thinking tokens: Extracted from standard completion tokens to ensure they are billed accurately at the correct model-specific rate.
- Grounding requests: Added flat-fee metering for Google Search by tracking grounding_metadata across both streaming and non-streaming responses.
- Pricing updates: Corrected stale rates for Gemini 2.5 Flash output, cached tokens, thinking tokens, and grounding requests.
* fix: correct Gemini 2.x metering rates and harden grounding capture
Pricing corrections (verified against ai.google.dev/gemini-api/docs/pricing):
- gemini-2.5-flash output is $2.50/M, not $1.00/M: restore
completion_tokens to 250 and bill thinking_tokens at the same output
rate (250). The previous 100 under-billed output ~60%, and this is the
provider's default model.
- gemini-2.5-flash cache read is $0.03/M: restore cached_tokens to 3
(the 7.5 value over-billed).
- Grounding with Google Search is $35 / 1,000 requests for Gemini 2.x
models and $14 / 1,000 for 3.x. Set grounding_requests to 3_500_000
for gemini-2.0-flash, gemini-2.5-flash, gemini-2.5-flash-lite and
gemini-2.5-pro; 3.x models keep 1_400_000.
Streaming robustness:
- In create_chat_stream_handler, don't let a later extra_content chunk
without grounding_metadata overwrite an earlier one that carried it,
so grounding requests are still metered.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---------
Co-authored-by: Daniel Salazar <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Include 'butler' in the RECOMMENDED_APP_NAMES array in RecommendedAppsService so it appears in the recommended apps list. Updates src/backend/services/apps/RecommendedAppsService.ts.
Revamps the phone verification dialog and adds reusable phone helper modules.
- Heavily refactors UIWindowPhoneVerificationRequired.js to add a searchable inline country picker, national-number live formatting, country autodetect for "+" input, WebOTP auto-fill support, resend cooldown timer, accessibility improvements, proper teardown/cleanup, and last-used country persistence.
- Introduces helpers/src/helpers/country_codes.js (get_country_list, flag emoji) which builds a localized list of dialable countries from libphonenumber-js and Intl.DisplayNames.
- Introduces helpers/src/helpers/phone.js with thin wrappers around libphonenumber-js: format_phone_as_you_type, inspect_phone, phone_example_for, detect_country_from_input.
- Adds English i18n strings for the new UI in src/gui/src/i18n/translations/en.js.
These changes keep submission canonicalization using libphonenumber-js (E.164), improve UX for selecting/formatting numbers, and make the country list and phone utilities reusable across the app.
The healthcheck was using 'puter.localhost:4100/test' which fails for
two reasons:
- 'puter.localhost' does not resolve inside the container; the service
listens on all interfaces at port 4100, so 'localhost' works
- '/test' endpoint does not exist; use '/' which serves the shell
This caused the container to be perpetually marked as unhealthy.
Fixes#3191
* ci: add docs build workflow
Add CI workflow that runs npm ci and npm run build in src/docs
whenever files under src/docs change in a pull request.
Closes#3225
* ci: also trigger docs build when workflow file changes
* ci: address docs build workflow review
* fix: coerce Mistral image_url parts from object to string
Mistral's API expects image_url content parts to be a plain string URL,
not the OpenAI-style { url: string } object that process_input_messages
produces. Without this coercion, sending an image to any Mistral model
that supports vision (mistral-small, mistral-medium, mistral-large,
ministral-*) results in a request error from the Mistral SDK.
Add #coerceImageUrls() as a private method on MistralAIProvider that
maps { type: 'image_url', image_url: { url } } -> { type: 'image_url',
image_url: url } for every content part in every message. Messages with
plain string content are left untouched, as are parts whose image_url is
already a string.
Add four unit tests covering: object-to-string coercion, already-flat
strings, plain string message content, and mixed text+image content.
* style: use plain ASCII dashes in comment section divider
* Add docs fixes for deployment and workers pages
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* handle router edge case
* explicit info about wildcard requiring name
---------
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Minor fix deployment docs
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
---------
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* fix(docs/search): show loading state and replay query when index isn't ready
Previously performSearch rendered "No results found" while the
MiniSearch index was still loading, which is misleading on slow
networks. Track the last query as pendingQuery and replay it from
fetchSearchIndex once the index is ready; render a distinct
"Failed to load search index" message on fetch error so the
loading message doesn't persist indefinitely.
Fixes#3180.
* fix(docs/search): close stale-replay race and consolidate status messages
Drop pendingQuery synchronously in the input handler when the query
falls below the minimum length, so a fetchSearchIndex that resolves
within the 300ms debounce window after the user clears the field can
no longer replay the cleared query into an empty input.
Move the idle, loading, and failed copy into a single
renderSearchStatus(kind) helper so the four .search-no-results
renderings can only drift in one place.
* feat(database): add postgres database client
Adds the PostgreSQL database client, native bootstrap migration, SQL preparation helpers, and database config/factory wiring.\n\nRefs #3165.
* feat(database): make backend queries postgres-aware
Updates runtime SQL call sites for database-specific booleans, identifiers, insert-ignore, upserts, JSON extraction, intervals, and Postgres insert ids.\n\nRefs #3165.
* test(database): cover postgres client behavior
Adds unit coverage for SQL preparation, factory selection, write-result mapping, and transaction rollback/commit ordering, plus an env-gated PostgreSQL integration flow.\n\nRefs #3165.
* docs(self-hosting): document postgres database setup
Adds PostgreSQL configuration examples and migration path guidance for self-hosted deployments.\n\nRefs #3165.
* fix: harden postgres oidc tests
* fix(postgres): normalize query results and SQL prep
* fix(user): preserve normalized cache booleans
* test(postgres): run integration coverage with pgmock
* tests: add way to run all tests with postgres though slow
Also adding note that postgres is not in active use so might not work out the box
---------
Co-authored-by: Daniel Salazar <[email protected]>
* feat: worker sessions get their own kind + per-(user, app, worker_name) row
Schema
------
- mysql_mig_11.sql + sqlite 0054: add idx_sessions_user_worker_active,
a partial unique index over (user_id, app_uid, meta.worker_name) for
kind='worker' rows. Active worker sessions are deduped by that triple
so each named worker gets its own session row and they don't fight
the existing idx_sessions_user_app_active (which still constrains
kind='app' only). app_uid is allowed NULL for user-scoped workers
with no app binding.
SessionStore
------------
- getOrCreateWorker(userId, { appUid, workerName, ... }): mirrors the
getOrCreateApp pattern — cache lookup, partial-unique re-SELECT on
insert-ignore, all keyed on the worker triple. expires_at lands at
WORKER_WINDOW_SECONDS (~99y) so the worker doesn't have to re-mint
on any cadence.
- #cacheKeyWorker + #allCacheKeysForRow worker branch so revoke /
update invalidates the worker cache view alongside the by-uuid one.
AuthService
-----------
- createWorkerSessionToken(user, workerName, meta?) now takes the
workerName explicitly and routes through getOrCreateWorker. Emits
the same { session, token, gui_token } shape but both JWTs carry
{ worker: true, worker_name }.
- createWorkerAppToken(actor, appUid, workerName) likewise — JWT
carries the worker_name claim so a verifier can tell two workers
under the same app apart without a DB round-trip.
- Both methods 400 on empty workerName.
WorkerDriver
------------
- Five auth-mint call sites swapped over: app-bound deploy (3x:
appId branch, actor.app fallback, hot-reload redeploy), user-bound
fallback (2x: cold deploy, hot-reload). All pass `workerName` so
the worker's session row is naturally idempotent across redeploys.
GUI manage-sessions
-------------------
- sessionTitle adds a kind='worker' branch ("name (app)" for
app-scoped workers, just "name" for user-scoped), pulling worker_name
from the meta-spread that listSessions already surfaces.
- en.js adds ui_session_kind_worker.
* fix(workers): MySQL JSON_EXTRACT quoting + revoke cache invalidation +
SQLite NULL-distinct in worker index
Three real bugs in the worker session plumbing from the prior commit,
plus a misleading comment. Schema design kept (worker_name lives in
`meta` rather than a dedicated column) per offline review:
1. `#selectWorkerRow` compared `JSON_EXTRACT(meta, '$.worker_name')`
directly to a bind parameter. MySQL's `JSON_EXTRACT` returns a
JSON-typed value with embedded quotes (`"name"`, not `name`), so
the comparison never matched. After the first INSERT, every
follow-up getOrCreateWorker call missed the existing row in the
SELECT, hit INSERT-IGNORE, then missed again in the re-SELECT —
the caller would receive whatever the INSERT-IGNORE returned (a
no-op row in conflict cases). Wrap with `JSON_UNQUOTE` on MySQL
via `db.case`; SQLite's `json_extract` already returns the
unwrapped scalar so it keeps the literal form.
2. mig_11's generated `worker_unique_key` had the same JSON-quoting
bug. Mirror the fix: `IFNULL(JSON_UNQUOTE(JSON_EXTRACT(...)), '')`
so the concatenated unique key is a plain string that lines up
with what `#selectWorkerRow` now binds against.
3. SQLite UNIQUE indexes treat NULL columns as distinct (per the SQL
standard), so two user-scoped workers (app_uid NULL) with the same
worker_name would both insert. Wrap the index expression with
`IFNULL(app_uid, '')` so they correctly conflict — matches the
MySQL side's `IFNULL` in the generated column.
4. `removeByUuid` / `revokeCascade` SELECTed only the identity
columns (no `meta`), so `#allCacheKeysForRow`'s worker branch
couldn't read `meta.worker_name` and the composite
`sessions:v2:worker:<user>:<app>:<name>` cache key survived
revocation. Up to CACHE_TTL_SECONDS (15min) afterwards,
getOrCreateWorker would short-circuit to the cached (revoked) row.
Add `meta` to both SELECTs; the existing meta-parsing logic in
`#allCacheKeysForRow` handles the rest.
* feat (put-1019 put-1021): v2 auth revoke endpoints + silent v1->v2 token migration
PUT-1019 (AUTH-5): full revoke-endpoint coverage
- /logout: soft-revoke web session + its asset cookies via revokeCascade
(app sessions and access tokens survive)
- POST /auth/revoke-session: cascade per row kind (web/app/access_token/asset)
- POST /auth/revoke-all-sessions: revoke all web rows for user; optional
include_apps=true nuclear option; gated by userProtected (cookie-only)
- revokeAccessToken: soft-revoke matching access_token row in addition to
removing access_token_permissions
- All revokes are UPDATE revoked_at = now(); no DELETE statements remain
PUT-1021 (SDK-1): backend POST /auth/migrate-token
- v1 access_token/app -> mint matching-kind v2 token, idempotent on
(auth_id, kind, token_uid)
- v1 web/session -> 409 { code: "reauth_required" } (interactive relogin only)
- Same-origin / signed-referer hardening; rate-limited per IP and auth_id
- Gated by auth.allow_v1_tokens; emits puter_token_v2 cookie for app-in-browser
DB migrations: mysql_mig_10, sqlite 0053 (sessions.access_token_uid column)
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(put-1019): reject self-revoke; enrich list-sessions response
- handleRevokeSession refuses uuid === req.actor.session.uid (use /logout
instead). The cookie that authenticated the call should never be the
target of a self-revoke — the response can't write fresh auth state
and the client ends up with an ambiguous identity. revoke-all-sessions
still has the explicit include_current opt-in for the nuclear case.
- AuthService.listSessions now joins the apps table for kind='app' rows
(returning { uid, name, title, icon } so the manage-sessions UI can
render the authorizing app without a second round trip), surfaces
kind / expires_at / label / last_ip / created_via, and filters out
asset rows (per-cookie children of web rows, revoked transitively via
cascade — surfacing them as standalone entries would be confusing).
- Sort order: current session first, then most-recently-active. UI
relies on this to anchor "you are here" at the top.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(put-1019 put-1021): review nits — origin normalization, cookie fallback, types, migration order
B1: createAccessToken.options.expiresIn widened to string | number.
The impl (#hardExpiryFromExpiresIn) and existing callers/tests use
jsonwebtoken-style strings ('1h', '30d'); narrowing to number forced
unsafe casts at every call site. Cast at the single sign() boundary
where jsonwebtoken's typed template-literal SignOptions clashes
with the wider runtime contract.
B2: Inline comment on the DELETE in access_token_permissions. The
AUTH-5 "no DELETE on revoke" rule scoped to the `sessions` table
(where the cascade graph + audit trail matter). Permissions rows
are the grant manifest for an active token — once its session is
soft-revoked they're dead-weight cache entries. A future audit
requirement would land as a `revoked_at` column on this table,
not a behavior change in this PR.
B3: handleMigrateToken now sets the puter_token_v2 cookie (with the
shared sessionCookieFlags + httpOnly) when the migration result
is kind='app'. The endpoint is already gated on Origin so the
caller is by definition in a browser; access tokens deliberately
skip the cookie since they're programmatic.
B4: #isMigrateTokenOriginAllowed normalizes both incoming origin and
config.origin / allowlist entries (trim + strip trailing slash +
lowercase) before equality. A misconfigured `config.origin =
"https://puter.com/"` would otherwise reject every same-origin
browser call.
B5: Replaced 4x `this.config.cookie_name!` non-null assertions in
AuthController with `(this.config.cookie_name ?? 'puter_token')`.
IConfig is `Partial<IConfigOptional>` so cookie_name is undefined
at runtime in some deployments / test setups; the fallback matches
the pattern in userProtected / OIDCController / puterSite.
B6: MySQLDatabaseClient sorts migrations numerically by trailing
integer instead of lexically. Existing files use unpadded names
(`mysql_mig_<N>.sql`), so plain `.sort()` ran mysql_mig_10 before
mysql_mig_2 — a future migration that depended on _2..9 running
first would break. Non-numeric filenames fall through to
localeCompare for determinism.
B7: Restored the docstring for SessionStore.getOrCreateApp's
`opts.auth_id` ("Stable per-user identity (survives re-login);
carried on every v2 JWT so manage-sessions can group by identity")
— the previous edit truncated it to a fragment.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* test + feat: backend test coverage for PUT-1019/1021 review fixes + worker session methods
Tests
-----
- compareMigrationFilenames (new) covering B6 numeric-sort: confirms
mysql_mig_10.sql lands after mysql_mig_9.sql; non-numeric files sort
after numbered ones; stable for already-ordered input.
- listSessions (AuthService.test.ts): excludes kind="asset" rows;
enriches with kind/expires_at/last_ip/created_via; joins kind="app"
rows with the apps table; sorts current first then by last_activity
desc.
- handleRevokeSession (AuthController.test.ts): refuses self-revoke
with 400; still allows revoking a sibling session.
- handleMigrateToken (AuthController.test.ts): rejects missing/
disallowed Origin; tolerates trailing slash and uppercase Origin
(B4 normalization); returns 409 reauth_required for v1 web tokens;
does NOT set the cookie for access-token migration; DOES set the
puter_token_v2 cookie (httpOnly + sessionCookieFlags) for
app-under-user migration.
- SessionStore tests updated to import APP_WINDOW_SECONDS /
WEB_WINDOW_SECONDS rather than hardcoded 30/90 day values — the
windows just got bumped to 1y and the assertions need to follow
the constant.
Refactor
--------
- MySQLDatabaseClient exports compareMigrationFilenames so the sort
logic is unit-testable in isolation.
Worker tokens
-------------
- AuthService.createWorkerSessionToken(user, meta?): mints a new
kind="web" row tagged meta.worker=true, expires_at =
WORKER_WINDOW_SECONDS, returns { session, token, gui_token }
with worker: true on each JWT.
- AuthService.createWorkerAppToken(actor, appUid): mints a new
kind="app" row tagged meta.worker=true, expires_at =
WORKER_WINDOW_SECONDS, returns an app-under-user JWT with
worker: true. Note the existing idx_sessions_user_app_active
unique index will collide with an existing non-worker app
session for the same (user, app) — future schema work can
carve workers out of that uniqueness.
SessionStore.js: WEB/APP_WINDOW_SECONDS now 1y;
WORKER_WINDOW_SECONDS = 99y added for the worker path.
Full backend suite: 2172 passed / 16 skipped / 0 failed.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
* feat (put-1022 put-1023 put-1024): puter.js reauth handling + GUI v2 modal + silent v1 migration
PUT-1022 (PJS-1): puter.js handles 401 reauth_required
- Single apiCall wrapper intercepts 401 { code: "reauth_required" }
- Clears local token, emits puter.auth.reauth_required { reason, auth_id }
- Queues in-flight requests, re-issues after re-auth completes
- web/app: opens puter.com login popup forwarding auth_id
- gui: no-op (handled by GUI-1 modal)
- workers: surfaces as structured exception
- All token writes go through setAuthToken() (future-proofed for PJS-2)
PUT-1023 (GUI-1): web GUI reauth modal + v2 token storage
- Detects 401 reauth_required across http + websocket connect paths
- Soft modal preserves URL/window state; auth_id forwarded into login form
- Storage key migrated: auth_token -> auth_token_v2
- Cookie cleared on logout: puter_token (legacy) cleared, backend writes puter_token_v2
- Cross-tab propagation via localStorage storage events
- WebSocket revocation surfaces as same modal (no silent failure)
PUT-1024 (PJS-2): puter.js storage versioning + silent v1->v2 migration
- New storage key puter.auth.token.v2
- On SDK init: prefer v2 key; else legacy v1 -> silent POST /auth/migrate-token (SDK-1)
- On success: store v2, clear v1. On failure: fall back to PJS-1 reauth flow
- URL-param tokens (?puter.auth.token=, ?auth_token=) also run through silent migration
- Logout clears both keys; setAuthToken writes v2 only
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(put-1023): manage-sessions polish — hide self-revoke, fix modal stack, richer rows
Three issues surfaced during local testing of the manage-sessions UI:
1. The session row representing the caller's own cookie had a Revoke
button that, if used, left the client in an ambiguous identity state
(backend now rejects it too — see put-1019 backend fix). The button
is now omitted entirely when session.current=true; /logout remains
the right path for ending the active session.
2. The confirm-revoke prompt rendered behind the manage-sessions
window because both share the dominant z-index pool. UIAlert calls
now pass parent_uuid (the manage-sessions window's data-element_uuid)
plus stay_on_top, so the prompt stacks above its parent.
3. Each row only showed the bare uuid. Now renders: title (app
title for app sessions, label / "Browser session" / "Access token"
otherwise), app icon when applicable, kind / current badges,
created / last-active / expires (timeago, with absolute on hover),
and last_ip. App metadata is joined server-side per the backend
listSessions change.
Adds en.js strings for the new labels (ui_session_*).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(put-1022): tighten reauth replay + iframe postMessage hygiene
driverCall_ replay (G1): on 401 reauth_required, re-enter driverCall_
instead of calling replayXhrAfterReauth. The generic helper wires the
retried XHR through setupXhrEventHandlers which resolves with the
parsed response and silently skips driverCall_'s streaming detection,
usage-limit / email-confirmation handling, settings.transform, and
resp.result unwrapping — i.e. it would change the driver call API
contract on retry. One-shot via settings._reauthReplayed so a
fresh-token rejection bubbles up instead of looping.
responseType drift (G2): driverCall_ sets xhr.responseType from
settings AFTER initXhr (which captured the stale value into
xhr._puterReq). Mirror the mutation onto _puterReq so any replay path
builds the retry with the live config rather than the snapshot.
targetOrigin lockdown (G3): triggerReauth's parent.postMessage now
targets this.defaultGUIOrigin instead of '*'. The payload carries
reauth metadata + auth_id and is only meaningful to the GUI; '*'
would leak the signal to whatever frame happened to be embedding us.
event.source pinning (G4): the reauth wait listener also matches
event.source against globalThis.parent, not just event.origin —
origin alone admits any same-origin frame on the GUI domain.
event name alignment (G5): the SDK now emits and listens for
'puter.auth.reauth_required' (matches the documented name in the
PR / commit description). The old 'auth.reauth_required' key isn't
yet consumed externally so this is a safe rename.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Record the 2.4.2 release in the repo. The version was published to npm
(via prepublishOnly's `npm version patch`) but the bump was never
committed, leaving package.json stuck at 2.4.1 on GitHub.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- decrease ai related alerts and make errors not 500
- decreased bad fs controller checks to be 404s instead of 500s
- force bucket regions for now
- validate webdav perms for locking/unlocking
Delete the large TabFiles.js file (file browser tab) and update dashboard, styles, helpers, and init logic accordingly. Related changes in UIDashboard.js, dashboard.css, style.css, helpers.js and initgui.js remove or adapt references to the removed component and adjust UI/initialization and styling to account for the file-tab extraction. This reduces duplicate/legacy code and centralizes file browser functionality; review UIDashboard and helper updates to ensure file management features (navigation, upload, context menus, previews) are migrated or re-implemented as needed.
- When hovering main menu items, it must clear hover effects
- Two submenus under each other and diagonal moving
- separator hovering removes highlight but disabled item should do the same
- better way to remove focus
- when mouse leaves context menu highlight should be removed
* Restrict cross-origin cookie auth
Block the auth probe from using ambient session cookies when a browser Origin does not match the request Host. Explicit bearer, body, and x-api-key tokens continue to work for cross-origin SDK calls.
Co-authored-by: Codex <[email protected]>
* Normalize origin checks for cookie auth
Compare normalized origins for browser cookie authentication so default ports and protocol mismatches are handled consistently. Add coverage for default-port and protocol-mismatch cases.
Co-authored-by: Codex <[email protected]>
---------
Co-authored-by: Codex <[email protected]>
xAI's Voice pricing table lists Text to Speech at $15.00 / 1M characters,
but we were charging $4.20 / 1M characters (420 microcents/char), undercharging
by ~72%. Update the cost constant, listEngines pricing, and test expectation
to 1500 microcents/char.
Adds offline XAITTSProvider.test.ts covering voice/format selection,
request shape, error paths, and cost reporting. Spies on global fetch
(the provider's egress point) against a real PuterServer + live
MeteringService.
Closes#2998
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Adds offline ElevenLabsTTSProvider.test.ts covering voice/format
selection, request shape, error paths, and cost reporting. Spies on
global fetch (the provider's egress point) against a real PuterServer
+ live MeteringService. The companion integration test stays untouched.
Closes#2999
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Adds offline OpenAITTSProvider.test.ts covering voice/format selection,
request shape, error paths, and cost reporting. Mocks the OpenAI SDK at
the module boundary against a real PuterServer + live MeteringService.
The companion integration test stays untouched.
Closes#3000
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Adds offline OpenAIVideoProvider.test.ts covering parameter mapping
(size and seconds snapping to allowed values, input_reference
forwarding), polling for queued/in_progress jobs (under fake timers),
sora-2-pro size tiers (xl/xxl per-second pricing), per-second metering
on default tier, failure handling, and error paths. Mocks the OpenAI
SDK at the module boundary against a real PuterServer + live
MeteringService.
Closes#2993
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Adds offline GeminiTTSProvider.test.ts covering voice/format selection,
request shape (transcript framing + speechConfig), error paths, and
cost reporting (token-priced input + output:audio batching, including
the PCM-to-WAV wrapping path). Mocks @google/genai at the module
boundary against a real PuterServer + live MeteringService.
Closes#2997
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Adds offline TTSDriver.test.ts covering provider selection/dispatch
(args.provider, legacy driverAlias via Context.driverName, registered
fallback), voice/format param validation, error mapping, and metering
propagation (provider failures must not bill). Mocks each provider's
SDK / fetch boundary against a real PuterServer wired with credentials
for every TTS provider.
Closes#2995
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Adds offline AWSPollyTTSProvider.test.ts covering voice/format
selection (caller-supplied, language-derived, default-per-engine
fallback), SSML handling (TextType=ssml routing), request shape to the
AWS SDK SynthesizeSpeechCommand, engine validation, error paths, and
cost reporting. Mocks @aws-sdk/client-polly at the module boundary
against a real PuterServer + live MeteringService.
Closes#2996
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Adds offline VideoGenerationDriver.test.ts covering provider
selection/dispatch (args.provider, model-id resolution across the
unified catalog, Context.driverName fallback), parameter validation
(seconds + dimension snapping to model.durationSeconds /
model.dimensions, string coercion), error mapping (provider
HttpErrors pass through, SDK errors don't bill), and metering
propagation (driver-level dispatch reaches the provider's
incrementUsage). Mocks each provider's SDK boundary against a real
PuterServer wired with credentials for every video provider.
Closes#2991
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Mocks the OpenAI SDK at the module boundary, boots a real PuterServer
for the wired MeteringService, and exercises construction (xAI base
URL, missing-key throw), model catalog (default + verbatim list),
test_mode bypass, prompt validation, the credit gate (402 before SDK),
model resolution (alias and unknown-id fallback), and the success
path covering URL/b64_json branches plus per-image metering with the
expected microcent override.
closes#2988
Mocks the OpenAI SDK at the module boundary, boots a real PuterServer
for the wired MeteringService, and covers the three model families
the provider treats differently. Exercises construction, model catalog
(dall-e-2 default + verbatim list), test_mode bypass, prompt
validation, the credit gate (402 before SDK), DALL-E request shape
(allowedRatio snap, hd-only-on-DALL-E-3, per-size metering, url and
b64_json output), gpt-image-* token-priced metering (input + output
batched line items at the model rates), and the gpt-image-2 size
normalizer (pixel-budget snap to multiples of 16, 3:1 ratio clamp,
3840 edge cap).
closes#2990
Mocks the together-ai SDK at the module boundary, boots a real
PuterServer for the wired MeteringService, and exercises construction
(missing-key throw), model catalog (default + verbatim list), test_mode
bypass, prompt validation, the credit gate (402 before SDK), all three
pricing branches (per-MP × ratio, per-image, per-tier with resolution
map rewrite), request shape (togetherai: prefix stripping, dimension
snap to multiples of 8 with min 64, optional knob clamping/forwarding,
input_image → image_base64 aliasing), and output handling for both
url/b64_json branches plus the wrapped error message.
closes#2987
Mocks @google/genai at the module boundary, boots a real PuterServer
for the wired MeteringService, and covers both the generateContent
(Flash) and generateImages (Imagen) code paths. Exercises
construction (missing-key throw), model catalog (default + verbatim
list), test_mode bypass, prompt validation, the credit gate (402
before SDK), Flash request shape (aspect ratio, allowedRatio fallback,
inlineData → base64 data URL extraction), Flash 3-line metering
(input + output:text + output:image with the IMAGE-modality token
count), Imagen routing (generateImages, per-image metering,
empty-response and rai-filtered failure modes).
closes#2989
Boots a real PuterServer with API keys for every image provider
configured (so the driver indexes them all), drives
server.drivers.aiImage directly, and mocks each provider SDK at the
module boundary so the driver's routing is exercised without real
network egress. Covers 401 without actor, 400 on unknown model,
catalog dedup + sort, list() and getReportedCosts shape, provider
routing (id-based, lowercased lookups, args.provider override,
Context.driverName fallback), ratio normalization (width/height and
aspect_ratio "w:h" forms), and the ai.log.image audit event firing
both before the upstream call and even when it fails.
closes#2984
Boots a real PuterServer for the wired MeteringService, stubs global
fetch (the provider talks to the Cloudflare REST API directly with no
SDK). Covers construction, model catalog, test_mode bypass, prompt
validation, the credit gate (402 before fetch), request shape (JSON
body, account-scoped /ai/run/<model> endpoint, apiBaseUrl override,
multipart routing for requiresMultipart models, steps clamping
[1,50]), output extraction (binary image/* → base64 data URL, JSON
envelope image with output_format → MIME, http(s) URL passthrough,
success:false 400, non-2xx 400, missing image 400), and per-scheme
cost components (tile-plus-step and flux2-klein-9b-mp split).
closes#2985
Mocks the groq-sdk at the module boundary, boots a real PuterServer
for the wired MeteringService, and exercises construction, model
catalog/resolution, request shape (max_completion_tokens rename, tools
passthrough, content blanking when paired with tool_calls), non-stream
+ streaming output (including Groq's x_groq.usage envelope deviation),
error mapping, and moderation.
closes#2977
Mocks @anthropic-ai/sdk at the module boundary, boots a real
PuterServer for the wired MeteringService/stores/FSService, and
exercises construction (long timeout for Opus 4.7 thinking), model
catalog/resolution, request shape (tool_choice locking, system message
extraction, OpenAI-shape tool_calls → Claude tool_use blocks, tool
role → user tool_result, opus-4.7 temperature drop, adaptive thinking
config, enabled budget thinking + temperature=1 for older Sonnet),
non-stream output (input/output/cache token metering), streaming via
the message_start/content_block_*/message_delta/message_stop event
graph for both text_delta and input_json_delta tool_use, and the
moderation throw.
closes#2958
Mocks the OpenAI SDK at the module boundary, boots a real PuterServer
for the wired MeteringService/stores/FSService, and exercises
construction, model catalog (responses_api_only filtering), argument
validation (messages-array gate, web_search routing — error when no
sibling, delegation when one is set), request shape (max_completion_
tokens rename, gpt-5 vs non-gpt-5 reasoning_effort/verbosity gating,
stream_options gating), non-stream + streaming output (cached-token
splitting), the moderations.create flagging branch (above and at the
0.8 threshold), and error mapping.
closes#2959
Boots a real PuterServer with an ElevenLabs apiKey configured, drives
server.drivers.aiSpeech2Speech, and stubs global fetch (the real
network egress for ElevenLabs). Covers getReportedCosts mirroring
costs.ts, argument validation (test_mode bypass, missing actor 401,
missing audio 400), the credit gate (402 before fetch), the success
path (POST URL with voice id, default + override voice/model, optional
knobs forwarded via FormData and search params, audio stream return
shape), per-second metering math, and error mapping when ElevenLabs
returns a non-2xx body.
closes#2963
Mocks the OpenAI SDK at the module boundary, boots a real PuterServer
for the wired MeteringService, and exercises construction, model
catalog/resolution, request shape (max_tokens, tools, stream_options,
DeepSeek's content-blanking and tool-result system-message workarounds),
non-stream + streaming output paths, error mapping, and moderation.
closes#2976
Mocks the together-ai SDK at the module boundary, boots a real
PuterServer for the wired MeteringService, and clears the kv-cached
model list between tests. Covers construction, model catalog (kv
caching, embedding-type filtering, alias flattening, synthetic
model-fallback-test-1), request shape (togetherai: prefix stripping,
max_tokens omission, tools/stream_options gating), model resolution,
non-stream + streaming output (with input/output cost-key remap),
error mapping, and moderation.
closes#2981
Mocks both the OpenAI SDK and axios at the module boundary, boots a
real PuterServer for the wired MeteringService, and clears the
kv-cached model list between tests. Covers construction (default and
custom apiBaseUrl), model catalog (axios fetch + kv caching, openrouter:
prefix on ids, openrouter/auto filtering), the openrouter/auto disallow
gate, request shape (prefix stripping, usage:{include:true}, stream
options, retry-without-max_tokens on context-length errors, rethrow on
unrelated errors), both cost-calculator branches (cost-bearing vs
per-token fallback) for non-stream and streaming, and moderation.
closes#2980
Mocks the OpenAI SDK at the module boundary (Gemini speaks the
OpenAI-compat dialect at the v1beta/openai endpoint), boots a real
PuterServer for the wired MeteringService, and exercises construction,
model catalog/resolution, request shape (max_completion_tokens rename,
cache_control stripping, stream_options gating), non-stream + streaming
output (cached-token splitting in the metered usage shape), error
mapping, and moderation.
closes#2961
Drops the local makeMeteringStub in favour of a live PuterServer-wired
MeteringService. The OpenAI SDK is still mocked at the module boundary
(the real network egress point); the mock now also exposes a default
export so the test server can boot every chat provider (some import
the default and read .OpenAI off it). Aligns with AGENTS.md: "Prefer
test server over mocking deps."
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
* tests: add tests for OCRDriver: [#2964]
Adds offline OCRDriver.test.ts covering both providers:
• test_mode short-circuit; argument validation (missing actor, missing
source, unknown provider, AWS/Mistral not configured)
• aws-textract: raw-bytes vs S3Object source selection (regional
client when fsEntry has a bucket), block normalisation (PAGE/WORD/
TABLE filtered, LINE/LAYOUT_TITLE → text/textract:* blocks),
402 on insufficient credits, per-page metering
• mistral: image vs PDF chunk packaging (image_url with base64 data
URL vs document_url with documentName), pass-through of pages /
annotation / image-limit options, markdown → LINE-block
normalisation with page indices, per-page metering, additional
annotations metering when bbox/document annotation formats are set
• default-provider selection (AWS preferred → Mistral fallback →
500 when neither is configured)
• getReportedCosts mirrors costs.ts
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* tests: OCRDriver uses setupTestServer + real fs/metering
Drops the manual config/clients/stores/services stub apparatus and
the loadFileInput mock in favour of the live wired driver from
server.drivers.aiOcr. The Textract and Mistral SDKs are still mocked
at the module boundary (the real network egress points); inputs go
through the real loadFileInput against real fs/store wiring (data
URLs for most cases; FSService.write produces a real fsEntry for the
PDF documentName test). Aligns with AGENTS.md: "Prefer test server
over mocking deps."
The S3Object-source/regional-client assertion was dropped because it
isn't deterministic against the in-memory S3 store and the driver's
per-region TextractClient cache leaks across tests. That branch is
better exercised by a real-cloud integration test.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
* tests: add unit tests for MoonshotProvider with mocked client: [#2979]
Adds offline MoonshotProvider.test.ts covering construction, model
resolution and aliasing, request shape (hardcoded max_tokens=1000,
stream_options gating, tool_use → tool_calls hoisting), vision-model
image-inlining hook, non-stream and streaming output, error logging
and rethrow, and metering. Existing integration test is left
untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* tests: MoonshotProvider uses setupTestServer + real metering
Drops the local makeMeteringStub in favour of a live PuterServer-wired
MeteringService. The OpenAI SDK is still mocked at the module boundary
(the real network egress point); inlineHttpImageUrls is mocked because
verifying the provider invokes it for vision-capable models is the
unit's concern. Aligns with AGENTS.md: "Prefer test server over
mocking deps."
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
* tests: add unit tests for MistralAiProvider with mocked client: [#2978]
Adds offline MistralAiProvider.test.ts covering construction, model
resolution and aliasing, request shape (camelCase maxTokens, tool_calls
→ toolCalls and tool_call_id → toolCallId rewrite, complete-vs-stream
routing), non-stream output with the camelCase usage coercion,
streaming with the chunk.data unwrap and toolCalls deviation, error
mapping, and metering. Existing integration test is left untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* tests: MistralAiProvider uses setupTestServer + real metering
Drops the local makeMeteringStub in favour of a live PuterServer-wired
MeteringService. The Mistral SDK is still mocked at the module
boundary (the real network egress point); everything else runs through
the real wired graph. Aligns with AGENTS.md: "Prefer test server over
mocking deps."
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
* tests: add unit tests for PuterAIController: [#2965]
Adds offline PuterAIController.test.ts with stubbed drivers covering:
route registration (paths and per-route auth options), app-actor
gating across all four chat-proxy routes, body validation (messages
must be an array; openaiCompletions prompt must be a string;
openaiResponses provider must be openai-responses), driver delegation
and request shaping (model + messages + provider defaults), response
shaping for OpenAI chat-completion / text-completion / Responses
envelopes and Anthropic message envelope (text + tool_use blocks),
SSE streaming with [DONE] terminator, model-listing endpoints with
hidden-id filtering and 501 when list() is unavailable, and the
HMAC-gated video proxy's input/expiry/secret guards.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* tests: PuterAIController uses setupTestServer + spy on aiChat.complete
Drops the manual driver/controller stub apparatus (makeStubDriver,
makeController, custom dep builders) in favour of the live wired
controller from server.controllers.puterAi. Each test that needs a
canned chat completion result spies on server.drivers.aiChat.complete,
which keeps the unit-of-test focused on the controller (validation,
response shape, SSE streaming) without dragging in chat-driver model
resolution and credit checks. Aligns with AGENTS.md: "Prefer test
server over mocking deps."
The "500 when URL signature secret missing" assertion was dropped —
the default config wires a secret, so the branch is unreachable in
the live test server. Replaced with the equivalent 403-on-invalid-
signature assertion which actually exercises the HMAC gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
* tests: add unit tests for ZAIProvider with mocked client: [#2983]
Adds offline ZAIProvider.test.ts covering construction, model
resolution and aliasing, GLM-specific request shape (custom params,
cache_control stripping, user_id derivation, stream_options gating),
non-stream output with reasoning_content normalisation, streaming
deltas (text and tool_use), error mapping, and metering. Existing
integration test is left untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* tests: ZAIProvider uses setupTestServer + real metering
Drops the local makeMeteringStub in favour of a live PuterServer-wired
MeteringService. The OpenAI SDK is still mocked at the module boundary
(the real network egress point); everything else — config, Context,
metering recording — runs through the real wired graph. Aligns with
AGENTS.md: "Prefer test server over mocking deps."
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Covers `Messages` (normalize_single_message / normalize_messages /
extract_and_remove_system_messages / extract_text), `FunctionCalling`
(normalize_json_schema / normalize_tools_object /
make_openai_tools / make_claude_tools), and `Streaming`
(AIChatStream / AIChatMessageStream / AIChatTextStream /
AIChatToolUseStream). All three are pure data transforms — Streaming
is exercised against a real `AIChatStream` wired to a buffering
Writable, so no method-level spies are needed.
Closes#2956
Covers the Puter↔OpenAI shape conversion (`process_input_messages`,
`process_input_messages_responses_api`), usage extraction
(`extractMeteredUsage`, `create_usage_calculator`), the streaming
handlers (`create_chat_stream_handler` and the responses-API variant),
and the non-stream `handle_completion_output[_responses_api]` paths
including moderation, missing-content, and per-provider deviations.
Streaming is exercised via a real `AIChatStream` wired to a buffering
Writable so no method-level spies are needed.
Closes#2955
Covers the early-return guard when the homepage service is unwired,
shell rendering at /, /settings, /dashboard, /action, /@:username,
canonical_url + meta inheritance from gui_params, /app/:name (404 +
shell, embedded app metadata, JSON / object metadata, malformed JSON),
and /show/* launching the explorer with the trimmed path.
Closes#2973
Covers the provider listing route, the auth-start redirect (404 / 400 /
500 / 302), all three callback flows (login / signup / revalidate)
including state validation, suspended accounts, unconfirmed-email
linking refusal, redirect_uri origin clamping, and the
revalidate-done landing page. Routes are collected via PuterRouter and
invoked with stubbed services that return prefab data.
Closes#2966
Covers `loadFileInput` (validation, data-URL decoding, FS-path resolution,
maxBytes enforcement, ACL gating) and `inferFilenameFromUrlOrPath`. Stubs
stores/fsService with prefab data so tests don't require a real server.
Closes#2957
Covers each config-gated branch (client_libs_root, puterjs_root,
gui_assets_root, builtin_apps) using real temp directories so the
existsSync checks see real files instead of mocked module behavior.
Asserts both registration outcomes (which routes appear, on which
subdomains) and the file resolution rules for the puter.js / puter.dev.js
fallback.
Closes#2974
Extensions listening on the `puter.signup.validate` event can now set a
`code` field (in addition to `message`) when blocking signups. The code
is forwarded through HttpError and appears in the JSON error response,
giving clients a stable, machine-readable signal to act on.
Covers both the AuthController (password signup) and OIDCService paths.
Adds tests for the new field.
In the app environment, the prompt input was initialized using `options.value`, so `defaultValue` was not reflected.
This change ensures that `defaultValue` is used when provided, while keeping backward compatibility with `options.value`.
Add a default-on email confirmation gate that rejects users with
`requires_email_confirmation && !email_confirmed` on all authenticated
routes, returning 403 with `email_confirmation_required`.
Previously this was only enforced client-side via a GUI modal, meaning
direct API calls could bypass the check entirely.
Essential routes are exempted via `allowUnconfirmed: true`:
`/whoami`, `/logout`, `/send-confirm-email`, `/confirm-email`,
`/save_account`, `/get-anticsrf-token`, `/get-gui-token`,
`/session/sync-cookie`, `/auth/revoke-session`,
`/user-protected/delete-own-user`
No impact on temp users (`requires_email_confirmation` is false),
self-hosted deployments without email (flag is never set), or
unauthenticated routes (login, signup, password recovery, OIDC).
Co-authored-by: Claude Opus 4.6 <[email protected]>
- Add XAITTSProvider calling POST /v1/tts with 5 voices (eve, ara, rex,
sal, leo), speech tags, and configurable output format
- Add XAISpeechToTextDriver calling POST /v1/stt with support for
diarization, multichannel, text formatting, and xAI's native URL
param for remote audio files
- Register both providers in TTSDriver and driver index using existing
xAI API key from config
- Update puter.js client to route provider:'xai' for both txt2speech
and speech2txt, including listVoices/listEngines
- Add TypeScript types for new STT options (provider, format, diarize,
multichannel, etc.)
- Update txt2speech.md and speech2txt.md with xAI options and examples
- Add playground examples for both TTS and STT
Cost metering:
TTS: 420 microcents/char ($4.20/1M chars)
STT: 2778 microcents/sec ($0.10/hr)
Introduce a new Gemini TTS provider and wire it through the driver, client, docs, and examples. Adds src/backend/drivers/ai-tts/providers/gemini/GeminiTTSProvider.ts (Google GenAI client usage, PCM->WAV wrapping, metering, model/voice validation) and a costs table in providers/gemini/costs.ts. Registers the provider in TTSDriver, exposes the alias gemini-tts, and prefers it in provider selection. Updates puter-js client to recognize "gemini" provider/engine and route driver calls to gemini-tts. Documentation updated with Gemini options, a usage example, and a playground example HTML file.
* Remove gross retarded legacy tools and docs
* Remove tools scripts
* nuke the bs
* Remove contributor/self-hosting docs, update license
* Remove src/useapi package and update configs
* remove source map files
* Remove wiki submodule
* ok not everything is retarded, for example this one (v86) is cool but has to go for now
* Remove puter-wisp module
* Remove install.md
* Remove twisp submodule
* Remove epoxy-tls submodule
Remove the epoxy-tls submodule and its configuration: delete the gitlink at submodules/epoxy-tls and remove the corresponding entry from .gitmodules.
* Fixing issues with mobile contextMenu in puter env app where user couldn't tap on menu items and there was checkmark in wrong place
* context menu mobile version - handle submenus
* context menu mobile icon placement tweak
* Web Components for Puter SDK UI API
* Rest of the components
* component docs
* Revert file and directory picker changes
* style matching with puter.com and support for custom styling
* item divider style tweak
* Puter JS - Web Components tweaking
* alert default image fix
* context menu mobile style fix
* Fix app iframe height when app is in fullpage mode and has puter-js menubar
* menubar web component tweak
* Action sheet behavior for contextmenu on mobile also in env app
* replace alert icons
* context menu and menubar keyboard navigation, submenu mouse navigation improvements
* package-lock
* Improve submenu mouse hovering
Make the DB update in AppES awaitable so the write completes before proceeding (avoids race conditions). Also add invalidation of the Redis object key for the app UID in AppInformationService to ensure cached entries keyed by uid are cleared after updates.
When the Start button is clicked while the popover is already open,
close it instead of doing nothing. This fixes the toggle behavior
on Android Chrome where the menu stays open on repeated taps.
The `.popover-launcher` removal triggers the UIPopover remove event
handler which automatically cleans up the `has-open-popover` class.
Fixes#1681
* Consistent header with files
* custom order with draggable app icons
* mobile style tweak
* Drag apps to sidebar
* uninstall
* Add desktop switch button to home tab
Drag sidebar apps order
* Display app storage use in Usage tab
* Add context menu options to apps: open in new tab, open in window
* Sidebar apps section with taskbar/dock behavior
* add close button to sidebar app item
* prevent dashboard files items from being selected when dragging
* File contextmenu shows filename
Keep dashboard URL when opening items
* Fixed issue with mobile selecting
* sidebar toggling
* sidebar tooltips
* done!
* Files tab statusbar width fix
* Consistent header with files
* custom order with draggable app icons
* mobile style tweak
* Drag apps to sidebar
* uninstall
* Add desktop switch button to home tab
Drag sidebar apps order
* Display app storage use in Usage tab
* Add context menu options to apps: open in new tab, open in window
* Sidebar apps section with taskbar/dock behavior
* add close button to sidebar app item
* prevent dashboard files items from being selected when dragging
* File contextmenu shows filename
Keep dashboard URL when opening items
* Fixed issue with mobile selecting
* sidebar toggling
* sidebar tooltips
* done!
* Files tab statusbar width fix
* Remove desktop switch button from Home tab
* App uninstall fix
* mobile style tweaks for dashboard sidebar
* Usage tab header fixes
* style tweaks
* Disable Uninstall for certain system apps
* Use app title in uninstall modal
* Remove delete-data option from uninstall modal
* Update dashboard.css
* Update TabApps.js
---------
Co-authored-by: jelveh <[email protected]>
Remove the alpha notice banner and spacer from Peer documentation pages (Peer.md, connect.md, ensureTurnRelays.md, connect.md, serve.md). This updates the docs to stop advertising the Peer API as alpha and cleans up redundant banner markup.
When reload_on_success is set, use '/' as the fallback redirect URL instead of the current page (window.location.href). This prevents redirecting back to the signup page after a successful signup while preserving any explicit options.redirect_url value.
Move @esbuild/linux-x64 from dependencies to optionalDependencies in src/docs/package.json so the platform-specific esbuild binary is treated as optional. Update package-lock.json to reflect that change (adds platform-specific @esbuild entries, marks various packages as peer/optional, bumps @emnapi/wasi-threads to 1.2.1 and removes obsolete @emnapi core/runtime entries).
Do not unconditionally overwrite window.disable_temp_users during GUI init. If window.disable_temp_users is already set, keep it; otherwise fall back to options.disable_temp_users (or false). Added a clarifying comment explaining the behavior.
* Add kv.list() pagination playground example
Create an interactive playground example for kv.list() that
demonstrates cursor-based pagination with sample data (6 items,
2 per page). Also wire the pagination docs example to the playground
by changing the code fence from plain html to html;kv-list.
Fixes#2755
* fix: address review feedback - restore code example, create new pagination playground
* minor refactor
---------
Co-authored-by: Reynaldi Chernando <[email protected]>
* docs(kv): add sorting section with examples to list() API reference
Added sorting section with examples for lexicographic order and zero-padding for numbers.
* docs(kv): add sorting note to list() description
* feat(playground): add kv-list-sort example
* feat(playground): add kv-list-padding.html
* add Sorting examples to kv list on doc/src/examples.js
* docs(kv): add sorting examples to list.md examples section
* Refine copy and code example
* remove extra newline
---------
Co-authored-by: Reynaldi Chernando <[email protected]>
* Map context_length for OpenRouter models
OpenRouter's API returns context_length for each model, but it was not
being mapped to the context field in the model list response. This
caused the context window size to be missing for all OpenRouter models.
Closes#2347
* Add context field to MistralAI and OpenAI model definitions
Addresses reviewer feedback to support the context field in more
providers. All 9 providers now consistently expose context window
sizes via the context field.
- MistralAI: 16 models (context matches known window sizes)
- OpenAI: 27 models (128k for GPT series, 200k for o-series,
1M+ for GPT-4.1 and GPT-5.4)
Bump the .dashboard-sidebar width from 200px to 250px to provide more room for navigation items and improve layout spacing. min-width, background, and border styles remain unchanged.
* Display logged-in banner on homepage
Pass auth_user from the router into PuterHomepageService and show a logged-in banner when a user is present. _default.js now includes auth_user in the svc_puterHomepage.send call. PuterHomepageService.send signature was updated to accept auth_user; the service stores logged_in_user, forwards it to generate_puter_page_html, and injects a styled "Logged in as <username>" banner into the top of the page when available. This surfaces the current user on the GUI and exposes logged_in_user to page generation and addons.
* Include request path and logged in user in GUI params; remove banner
* fix: bad auth_user check for homepage
---------
Co-authored-by: Daniel Salazar <[email protected]>
Prevent a visual flash by hiding the apps grid until all app icons have loaded. Adds revealWhenLoaded($container) which removes the myapps-grid-loading class when images finish (or error). TabApps now caches the .myapps-container lookup and calls revealWhenLoaded after initial render, after search results, and after loading installed apps. CSS changes: add .myapps-grid-loading (opacity:0 + transition) and a small padding-right tweak for the search input.
Fetch both the installedApps and get-launch-apps endpoints in parallel, normalize recommended + recent launch items, and merge them with installed apps while deduplicating by name (launch items are prioritized). Update this._apps and render the combined list. Also simplify the app tile icon styling by removing border-radius and box-shadow for a flatter icon appearance. Error handling for failed loads is preserved.
Add /css/dashboard.css to the GUI asset lists so dashboard styles are served and bundled. Updated src/gui/puter-gui.json and src/gui/src/static-assets.js to include the new stylesheet.
* feat: add Anthropic Messages API compatibility layer
Add a new endpoint at /puterai/anthropic/v1/messages that implements the
Anthropic Messages API wire format, allowing clients using the Anthropic
SDK to point directly at Puter.
- New router translates between Anthropic format and Puter's internal
svcAiChat.complete() pipeline
- Supports non-streaming, SSE streaming (proper Anthropic event sequence),
and tool use round-trips
- Translates system field, tool definitions (input_schema -> parameters),
and tool_result content blocks
- Integration tests covering non-streaming, streaming, tool use,
Anthropic SDK compatibility, and system parameter
Closes#2554
* Fix authentication middleware
---------
Co-authored-by: ProgrammerIn-wonderland <[email protected]>
Prevent creating temporary users on unauthenticated first visits when the current action is the login or signup flow. The initgui else-if was expanded to include explicit checks for action !== 'login' and action !== 'signup', avoiding interference with explicit authentication routes and Turnstile challenges.
Use a single expression to derive the plan key (offering.name or 'free') and pass it to i18n when updating .bento-plan-name. This removes the previous ternary and duplicates, allowing dynamic offering names to be localized consistently.
Add missing English translations for plan tiers (free, basic, professional, business) to src/gui/src/i18n/translations/en.js and perform a minor formatting cleanup near billing messages to ensure keys are available for UI display.
Update TabHome to read the plan name from subscription.offering.name instead of subscription.plan_name. This adapts to the updated subscription object shape while keeping the fallback to the 'pro' translation and the 'free' label when no active subscription exists.
The taskbar uses position:fixed with bottom:5px, which gets hidden
behind the iOS Safari bottom toolbar on devices with home indicators.
Use env(safe-area-inset-bottom) to push the dock above the safe area.
The viewport-fit=cover meta tag is already present in initgui.js.
Fixes#2517
* fix: tighten up cors for socket io
* fix: allow socket io to push to rooms in redis
* fix: various socket io issues to improve cross session stability
Update check for suspended users on change_emeail and user-protected
endpoints. The `change_email` endpoint isn't using the auth middleware
because it infers from the token that was passed - this means it needs
explicit logic to check suspended users; before suspended users were
able to complete a a flow for changing their username if they already
started it before being suspended.
Update user-protected endpoints and configurable_auth so endpoints that
make sensitive account changes do not rely on cached information about a
user.
Introduce a new i18n key `add_to_desktop` with value 'Add to Desktop' to the English translations file. This provides a label for UI actions that add items or apps to the desktop.
* chore: upgrade git-date (used for release notes generator)
* chore: update vitest, vitest plugins, and svgo
* chore: run `npm audit fix`
* chore: run `npm audit --force`
* meta: empty commit to run git tests
* fix: tests gone because of a... syntax error?
I question GitHub's judgement of handling a syntax error in the file
that defines tests to run on PRs by pretending everything is fine and
passing all PRs. That to me seems like the worst possible way to handle
that. I think they should have done it not like that.
When a global ClickHouse client is available, run two parallel ClickHouse queries (JSONEachRow) to fetch per-app open_count and distinct user_count, parse results into Maps (parsing counts as integers). If ClickHouse is not present, fall back to the original MySQL aggregate queries. Apps list still comes from MySQL as before. This enables using ClickHouse for analytics performance while preserving the existing behavior as a fallback.
* fix: only set authToken if present for apps
* fix: keep bootstrap in url for app to do whatever
* fix: tests
* fix: app merging
just cleaning up how merging subdomain and canon apps work, namely, persisting data better and making sure its temp alias are deleted when appropriate
* fix: tests oom
Usage example:
const b = new puter.fs.Batch();
b.move('file_1.txt', '~/Trash');
b.move('file_2.txt', '~/Trash');
await b.end();
Chaining is also supported:
await new puter.fs.Batch()
.move('file_1.txt', '~/Trash')
.move('file_2.txt', '~/Trash')
.end();
This commit provides `move` and `delete` methods for Batch.
* feat: scope index_url uniqueness to hosted domains
Allows duplicate non-hosted origins while enforcing hosted-origin uniqueness and joining new app creation into an existing owner-matching hosted app UUID.
Restricts hosted index_url joins to unowned conflict apps, persists old-to-canonical app UID aliases in kvstore via sudo, and resolves aliases on read with parallel lookup to minimize latency.
* fix: allow merge for owned origin bootstrap apps
Restores hosted update/create merge when the conflicting app is the same-user auto-created origin bootstrap app while still blocking merges into normal same-owner apps to avoid multi-join behavior.
Adds non-blocking public hosted actor cookie mint/verify flow for non-private apps, sets actor context when available, and includes focused middleware/auth tests for security and performance-sensitive paths.
Adds an extension that presents an unauthenticated page for testing
various actions of Puter when it's under development. This action is
only meant to be used in development and is not loaded by default.
* feat: phase 2 enforce index url ownership and uniqueness
Adds write-time index URL uniqueness checks, expands hosted subdomain ownership enforcement to private hosting domains, and updates AppService tests for duplicate-index behavior.
* test: align app service comp fixtures with index url uniqueness
Updates regression test fixtures that intentionally exercise name conflicts and selection paths to use unique index_url values under the new one-index-per-app constraint.
* fix: exempt placeholder index url from uniqueness checks
Skips index_url uniqueness enforcement for the shared dev-center coming-soon URL in both AppService and AppES, and adds unit/comp regressions to keep one-index-per-app behavior for all other URLs.
Set reload_on_success explicitly when invoking UIWindowLogin so the UI does not trigger a page reload if the GUI is embedded in a popup. Previously the call forwarded a reload_on_success variable; now it uses !window.embedded_in_popup to prevent unwanted reloads in popup embeds.
* Revert "dev(oidc): hide unnecessary div"
This reverts commit 468558f8dc.
* fix(oidc): remove the "You will be asked to..." box properly
Earlier this was removed with a simple `display: none` in CSS to avoid
changing any logic, and the reason for that was OIDC had already gone
through significant testing, re-testing, and bug fixes; it became clear
that changes to the auth flow are fragile and the test surface is very
large. Now unfortunately what seemed like a smart decision to fix a
cosmetic issue with CSS instead of updating logic actually wound up
breaking the change username/password flow for non-OIDC users. This
commit removes this box properly and hopefully does not introduce a
subsequent terrible bug. Why would it? There's absolutely no reason I
can see that it would, but it would seem that nothing is safe, not even
CSS.
* temp: disable LRU cache of homepage for now
This is working fine but we're disabling it temporarily so we can test
specific future changes in isolation without managing two separate
branches.
* fix: put verify password text in the box
There's always something...
LRU cache the output of PuterHomepageService with a cache key based on
metadata that affects the output of the homepage (if an app was
specified, and if so what app was specified).
With this change in place we can afford more time processing this
output, for example to minify the output and reduce outbound bandwidth.
* add configurable GUI bundle
* also support CSS and puterjs custom location
* simplify ternary
* used bundled instead of use_bundled_gui
* Quote the string
* close script tag
* feat: phase 1 canonical app origin resolution
Adds cache-first canonical app uid resolution for origin-based auth flows with oldest-app selection and deterministic fallback, plus regression tests for hosted/non-hosted canonicalization behavior.
* refactor: use kv singleton for origin local cache
Replaces AuthService app-origin local canonicalization Map cache with kvjs singleton keys, applies TTL via EX on writes, and rotates local cache namespace on version bumps to avoid broad clears.
* refactor: target origin cache invalidation
Removes global app-origin cache version bumps and invalidates canonical origin cache entries by affected app index URLs from app.changed payloads. Also emits app and old_app data on update events for targeted invalidation.
* fix: avoid invalid old_app owner field in app change event
Removes owner_user_id access from AppES old_entity event payload because the entity model does not expose that property, fixing AppService comp upsert regression tests.
Convert newline characters in email reason text to <br /> tags and ensure proper escaping. Replaced {{reason}} with {{{nl2br reason}}} in the rejection template and registered an nl2br Handlebars helper that escapes HTML entities, transforms newlines to <br /> and returns a SafeString so line breaks render correctly in emails.
Each of these methods is only being called by get_app_icon_url. By
changing that ternary operator into an `if` block with an early return
we can move the bodies of these functions here. If one of the former
functions ever needs to be used in more than one place, it's easy to
extract it out of here agian - for now though let's not pay the cost of
jumping to another function definition while trying to understand this
code if we're not gaining anything from it.
* style(puterfs): update adherence to linter rules
* fix(puterfs): await queue items for fsentry actions
This will ensure some operations always function as expected in a
sequence, although it may incur some performance costs.
* dev(apps): add configuration to not use app icon subdomain
For some local configurations of Puter, such as using ngrok or a hosting
service that has limited subdomains or limited subdomain depth, the
static hosting subdomain for app icons is difficult to configure and may
not be viable. Since this is only done so an external geo-replicated
cache can be used, and it's always possible for Puter's backend to
generate an app icon, it should be possible to configure Puter's backend
to use icons from a URL hosted on the default subdomain.
Add a configuration parameter to the service "app-icon" called
"no_subdomain" which prevents Puter's backend from sending puter-icons
subdomain URLs or redirecting to them.
Also perform some small cleanup changes to make the existing code easier
to understand, including:
- rename `buildAppIconUrl` to `buildAppIconSubdomainUrl` to make it
clear which of the two types of icon URLs are being generated.
- replace `withAppIconUrl` with `get_app_icon_url` so it is possible to
get an icon URL without mutating an app object.
- make the `get_taskbar_items` helper use the same code for getting app
icons as the `get_apps` helper.
* fix(apps): test initializes a service with no config
The request object was not being passed to OIDC signup because it is
called by createUserFromOIDC which doesn't take a request object as a
parameter. It was decided to have `create_user` take the request object
from context if it's not specified rather than change the signature of
createUserFromOIDC.
I decided not to remove it because I really don't want to introduce a
bug right now while everything is finally working smoothly... I'm making
a note to properly clean this up later when it can be more easily tested
in isolation from everything else.
It turns out there are nuances between `puter.ui.authenticateWithPuter`
vs `puter.auth.signIn` - these don't do the same thing. The primary
difference is that `puter.ui.authenticateWithPuter` will display an
override if it's not triggered by a user action, whereas
`puter.auth.signIn` will not. This definitely suggests
`puter.ui.authenticateWithPuter` should be a caller of
`puter.auth.signIn` instead of implementing its own logic for handling
the popup - that makes this part of the code more fagile - but that
refactor is out-of-scope for this bug fix.
* feat: resolve private app hosts by index_url fallback
Adds a private-app lookup fallback for hosted subdomains without associated_app_id by matching owner-scoped index_url candidates built from request host and configured protocol.
* fix: redirect path
* fix: add new domains too
* fix, bootstrap url
* fix: bootstrap url
* fix: auto sign in puter pirvate app
* Fix: When a maximized window gets resized, Puter apps should be resized as well
* Fix maximixed selector to match any window with data-is_maximized (not just apps)
For convenience, switch flow if the user's action doesn't match the
system state when signing up or logging in with Google:
- If the user chooses "signup" but they already have an account, log
them into that account.
- If the user chooses "login" but they do not have an account yet,
create an account where their authenticated email address.
Adds a config flag to disable private app gate enforcement, structured middleware audit logs for private access decisions, and regression coverage for the disabled-gate path.
When a rate-limit scope is not configured, use a default configuration
instead of throwing an error. Display a warning about the unconfigured
rate-limit scope when the default is used.
* fix: disable broadcast for cache invalidation
* fix: remove broadcast for redis events for now
* fix: don't await cache invalidation for grant app permission
Fixes an error introduced in 4b8c46e where the page load is attempted to
be triggered by dispatching the login event, however the listener which
handles loading the page has not yet been registered.
* fix(oidc): OIDC login with puter.js popup auth
Bring login through OpenID Connect to a working state in the puter.js
popup authentication flow. This allows OIDC login to work with
third-party websites and apps.
* fix(oidc): error handling, use const (initgui)
Remove a condition that can be handled by the control flow logic
already implied by a try...catch, which also allows a variable to become
const, and log errors in this catch block.
* doc(oidc): make this comment a little more helpful
* clean(oidc): remove out-of-scope puter.js change
Remove a change that gives explicit overrides of the `defaultGUIOrigin`
paramter precedence over other sources of this value. This was necessary
when testing but might not be the correct fix for this issue.
Await the window.update_auth_data call in initgui.js so subsequent logic (such as popup spinner hiding and timing) runs after authentication data has been updated. This prevents race conditions where later UI code executes before auth state is fully applied.
Read user preference keys (show_hidden_files, language, clock_visible) into local variables with try/catch and sensible defaults, instead of fetching/parsing them inline. This prevents exceptions from missing or malformed KV entries and centralizes parsing for show_hidden_files. Also remove an extra blank line in initgui.js's AJAX complete callback.
Redirect to the login or signup page when there is an error signing in
or creating an account using OIDC, instead of displaying the error on a
new page. Alter the flow in cases where the suggested action is not the
same as the initial action taken by the user (based on the error case).
* fix nano banana pro pricing and resolutions
* move gemini resolution map into models.ts
* require gemini 3 model for specific resolution and price logic
* fix usageamount for mp models
UIItem makes a recursive call when there are multiple matching elements
in the `options.appendTo` argument. When calling recursively, the
`appendTo` property of the `options` object is mutated to a specific
element before the call, which expects that each call to UIItem occurs
synchronously. UIItem was made asynchronous by
9d598f7965
which resulted in a bug causing duplicate item icons in one directory
window instead of one item icon in each directory window after a file
upload when multiple instances of a window at the same directory
location exist.
Resolves#2514
we saw some errors with icons not allowed when being objects, this likely user error, but this adds some validation to at least avoid nulls being flagged wrong
* fix: allow healthcheck from other hosts
* fix: don't await for cache writes
we think some awaiting on redis writes was causing issues, we shouldn't need to await really, so removing now. will have optimistic caching for these
some style changes from a recently changed rule that seems to clash with prior one
Fixed: The gradient and progress bar on the usage details page is not very pleasant and looks inverted
Fixed: The billing card on the billing tab is still light mode
This code was previously lost because I edited `outcomeutil.js` instead
of `outcomeutil.ts`. We're not building into a `dist/` directory and
`tsc` has a most peculiar lack of generating a comment at the top of
output files stating something like "// GENERATED - DO NOT EDIT" as I've
seen from every other code generator or transpiler I've worked with.
This caused the bug with duplicate confirmed emails and "account not
found" during testing on the staging server.
I forgot that our transpile tooling requires manually gitignoring
individual javascript files because we don't actually create a full
generated build of backend.
During development a property named `success` was inverted to a property
named `failed` which resulted in an incorrect accessor reference with a
referring piece of code that wasn't updated. This is type error.
In lieu of knowing exactly what happened (probably more than one thing),
the "Disable 2FA" window was very broken. It was blank, but then after
fixing that all the actions were broken. There wasn't much value in
keeping the implementation though, because it was already inconsistent
with other flows - instead of fixing what was there it made more sense
to re-use the pattern of UIWindowChangeUsername and UIWindowChangeEmail,
creating UIWindowDisable2FA. After testing this, it works much better
(it actaully works), but there is a caching issue unrelated to the UI
implementation.
A component was removed and an html property was passed to
UIComponentWindow. This makes sense because UIWindow accepts an html
property, so rather than update the calling code it made more sense to
update UIComponentWindow to be more intuitive.
This button was useful during manual testing, but the re-authentication
flow for protected endpoints with OIDC users reliably invokes the popup,
so this is no longer necessary. Removing this button reduces clutter on
these screens and might make the flow easier for users to understand.
In implementing OIDC it became necessary to introduce the separation of
"GUI Tokens" and "Session Tokens". This breaks QR login because Puter
does not set the HTTP-only session cookie when logging in with this
flow.
Add a middelware to WebServerService to detect QR Code logins and set
the appropriate HTTP-only session cookie.
Use the openRevalidatePopup function in util/openid.js within
UIWindowChangePassword instead of re-implementing that functionality.
Additionally, normalize some of the code so it is more similar to
UIWindowChangeUsername and UIWindowChangePassword.
There is common functionality between all of the GUI code for actions on
protected endpoints. Update UIWindowChangeEmail and
UIWindowChangeUsername to both use a new utility function called
openRevalidatePopup in util/openid.js.
This file is called `openid.js` instead of `oidc.js` so that it's more
easily recognized by contributors who might be more familiar with the
name of the organization than the name of the standard itself.
After these changes, UIWindowChangePassword and the "disable 2FA" button
in UITabSecurity still need to be updated to use `util/openid.js`
instead of duplicating this functionality.
The justification for following DRY here instead of leaving the
implementation as-is is because these flows are particularly error
prone and will be difficult to maintain without this consistency. Some
subtle bugs I previously wasn't aware of got fixed in the process.
The OIDC re-authentication flow, which replaces password confirmation
for accounts that were created with OIDC and do not have a password, was
previously added to "change username" for manual testing of the
backend-side implementation. Add the re-authentication flow to the
remaining user-protected endpoints, which are:
- change password
- change email
- disable two-factor authentication
When using "change password" on a new account created via OIDC, the
account changes state to a passworded account which causes these flows
to use password confirmation as before instead of re-authentication.
The monthly number of username changes was hardcoded as `2`. Being able
to configure this value makes it easier to test the username change
flow. Hosters of OSS Puter may also find this configuration beneficial.
When users make sensitive changes to their account they are asked to
re-enter their password. This prevents a hijacked session from causing
futher damage.
Users created with the new OIDC flow do not necessarily have a password
set on their account, and they need to also be able to make these
changes. While removal of the password entry requirement for these users
would solve this problem, it would also make their accounts more
vulnerable. To solve this problem while maintaining the same security
standard for OIDC users, we need them to confirm via either 2FA or
re-authentication via OIDC. Since users aren't required to have 2FA, the
re-authentication via OIDC approach is also the minimum viable solution.
This commit adds OIDC re-authentication support for all endpoints under
UserProtectedEndpointsService, and makes updates to the
UIWindowChangeUsername dialog for manual testing.
Currently this implementation fails at the final submission to change
the username because of a separate issue with the correct authentication
token not being set; this is related to the separation of GUI tokens vs
http-only tokens.
This commit is rather monolithic. An attempt to split it up into smaller
changes proved too difficult (as well as frustrating) and I realized it
would absolutely increase the chance of having a broken commit (making
bisects more difficult) unless a lot of testing effort between commits
was performed, which would have very little benefit.
The changes in this commit include:
- Outcome utility used by SignupService for error handling
- SignupService, whichs implements re-usable create_user function
- Signup method in OIDCService
- flow-specific callbacks in OIDC (separates login from signup)
- **SEPARATE SESSION COOKIE AND GUI COOKIE**
- this change "rocks the boat" the most and has the highest likelihood
of causing problems
* Fixed issues where file list wasn't scrollable on mobile device, and tap'n'hold on file opened wrong context menu.
* Fixes text color of selected row
- [x] Fixes path navigation button transitions
- [x] Fixes path navigation button colors
- [x] Hide AppData folder
- [x] Hide hidden files
Fix ESM/CJS interop issue in unit tests with a hack that checks the
class name as a string instead of just doing an `instanceof` check. This
is assuming that the "rewriter must be a PermissionRewriter" error I'm
seeing on GitHub is because there are two different "copies" of
PermissionWriter due to ESM/CJS interop issues. If this is not the case,
then you're not reading this commit message on `main`; if this commit is
on `main` then that's exactly what went down.
Add the ability to request an app's root directory. A permission
rewriter is provided so that apps may refer to a permission when
requesting this access without knowing the path of the app's root
directory.
* Dashboard - Added files tab with basic folder and files listing.
* clearing out some linter errors
* Removed Public folder for now
* Added "modified" column
Subfolder are listed before the files
* Dashboard - files tab: context menu basics
* Dashboard - Files tab: subfolders can be opened. Items can be opened from more-menu or by double clicking on them
* Dashboard - Files tab: use UIContentMenu instead of custom menu for "more menu"
* Dashboard - Files tab: Delete implemented, Trash folder added
* refactored context menu rendering
* Implemented UIItem context menu functionalities
Added path
* Rename files and folders
* Rename fix
* tweaks and fixes
* WIP: Multiple selection, drag and drop, trash folder improvements
* clean
* Files handling improvements
* empty trash fix
* grid view
* grid view improvements
* update icons
context menu for multiple items shows file counts such as: Delete (9)
* Path navifation buttons
* better way to get user's directories
* Added icons for various file types
* Use puter.kv instead of window.user_preferences
* sorting and column resizing
* documentation of the functions
* Style update. Dashboard colors now as CSS variables
* refactored restoring from trash and emptying trash with proper callbacks & refreshing
* select documents folder by default
style tweaking
* Drag to select files and folders
* dark mode
* style tweaks
* removed some dead code..
* Keyboard shortcuts (copied from UIWindow - untested & in progress)
* Add drag-and-drop file upload support to Dashboard
Implement native file drag-and-drop from local filesystem to Dashboard:
- Add dragster handlers for main content area, sidebar folders, and folder rows
- Create uploadFiles() method with progress modal and directory refresh
- Add visual feedback CSS for drop zones (highlight on hover)
- Fix subfolder drop detection to prevent duplicate uploads
- Add callback support to unzipItem/untarItem for directory refresh
Co-Authored-By: Claude Opus 4.5 <[email protected]>
* style tweaks
* Fix cut/paste functionality in Dashboard
The cut operation was storing clipboard items as plain path strings, but
window.move_clipboard_items() looks for .item DOM elements which don't
exist in Dashboard (it uses .row elements). Added a Dashboard-specific
moveClipboardItems() method that calls puter.fs.move() directly with UIDs,
and updated cut to store objects with path and uid like copy does.
Co-Authored-By: Claude Opus 4.5 <[email protected]>
* Add mobile phone layout optimizations for Dashboard
- Add responsive CSS for screens ≤480px with two-row file list layout
- Wrap item-size and item-modified in item-metadata div for mobile grid
- Hide directories sidebar on mobile (use breadcrumbs for navigation)
- Reorganize header with breadcrumbs on top row, nav buttons below
- Add single-tap to open folders on touch devices
- Fix context menu "Open" action in Dashboard by adding onOpen callback
- Hide column headers on mobile since metadata is shown per-item
Co-Authored-By: Claude Opus 4.5 <[email protected]>
* Add CSS variables to Dashboard bento elements for dark mode support
Replace hardcoded color values with CSS custom properties throughout
the bento card components to enable proper dark mode theming. Adds new
variables for icon shadows, fancy headers, gradients, and usage bars
with appropriate values for both light and dark color schemes.
Co-Authored-By: Claude Opus 4.5 <[email protected]>
* Add spacebar image preview feature to Dashboard Files
Pressing spacebar on a selected image file now shows a full-size preview
popover centered over the files container. Features include:
- Toggle behavior: press spacebar again to close
- Auto-switch: selecting another file updates the preview
- Click outside to dismiss
- Works in both grid and list view
Co-Authored-By: Claude Opus 4.5 <[email protected]>
* Add floating action bar for multi-file selection in Dashboard Files
When 2+ files are selected, a floating action bar appears above the footer
with quick access buttons for common operations:
- Download, Cut, Copy, Delete for normal files
- Restore and Delete Permanently for items in Trash
The bar slides up with a smooth animation and disappears when clicking
the background to deselect. Includes mobile-responsive styling with
icon-only buttons on smaller screens.
Co-Authored-By: Claude Opus 4.5 <[email protected]>
* Add mobile select mode for multi-file selection in Dashboard Files
On mobile devices, users can now tap a select button in the path actions
to enter select mode. In this mode, tapping files toggles their selection
instead of opening them, with checkboxes appearing next to each item.
The floating action bar appears with 1+ selected items (vs 2+ on desktop)
and includes a Done button to exit select mode.
Co-Authored-By: Claude Opus 4.5 <[email protected]>
* Center empty directory message in Dashboard Files
Replace row-based layout with centered flex positioning for the
"No files in this directory" message, improving visual presentation.
Co-Authored-By: Claude Opus 4.5 <[email protected]>
* Fixed issue where clicking on files background created blue dot (drag rectangle selection bug)
* Image preview size tweak
* Add URL-based routing to Dashboard with hash fragments
Enable shareable/bookmarkable URLs for Dashboard tabs and file paths.
The URL hash reflects current location (e.g., #files/username/Documents,
#usage, #account). Browser back/forward navigation now works for folder
navigation, and direct URL entry loads the correct tab and path.
Co-Authored-By: Claude Opus 4.5 <[email protected]>
* fixing draggable styles
* drag clone fixes
* Add smart column resizing and filename truncation to Dashboard Files
- Add middle-truncation for filenames preserving extensions (e.g., "longfile…name.txt")
- Double-click column resize handles to auto-fit content width
- Reset saved column widths to defaults if they don't fit smaller screens
- Apply truncation to both list and grid views
Co-Authored-By: Claude Opus 4.5 <[email protected]>
* Separated dashboard styles into dashboard.css and loading it dynamically only when navigating to /dashboard
* Files and folders can be dropped over path items in order to move them. This is useful when you want to move items into parent folders.
* Use custom context menu component for item-more menu on mobile devices
* Created callback system to undo_last_action so that dashboard file list would always refresh after undo operation
* gray update
* hovering folder while dragging items opens the folder
* Fixed issue with hover-dropping
* Fixed chained drag-over-folder-and-hover system
* dark mode tweak
* - [x] Right clicking on background of an empty folder doesn't trigger context menu
- [x] Right clicking on path should keep path item active
- [x] Sidebar folder don't have context menu
- [x] Drag item should be opaque so I can see through it
- [x] Transition animation issue when hovering path action buttons
- [x] Double clicking on column resizer: add padding
- [x] Load dashboard.css before loading the page content
- [x] Mobile: opening menu, clicking outside menu should close it
- [x] Mobile: on smaller screen there should be still two columns
- [x] Mobile: file name truncation in grid view doesn't work
* Added missing badges
* WIP has_worker
* display worker icon on the .js files which are workers
* Display worker badge on worker files, hovering badge shows worker URL.
Worker URL added to file properties modal.
* disable "publish as website" from context menu for folders that are already published
* Loading spinner for Files
* tweaks and fixes
* Files clean loading spinner
* Files: worker URL in the properties modal is clickable link
* use custom tooltip to display worker URL
* refactored renderDirectory
* Fix Sidebar folder highlighting
* add Beta badge on Files tab
* Implemeting socket based item updating
* Put newly added items into the position where they would land when list is sorted
* restore from trash refresh fix
* Revert "Revert "Adds worker badge to .js files which have been registered as workers. Hovering badge shows popover with worker URL. File properties modal also shows clickable worker URL. (#2462)""
This reverts commit c2750f2ca1.
* worker in stat
* update puterjs and frontend code to support returnWorkers in stat
* Better way of handling worker info
* Fixed issue where right clicking on background opened two instances of a context menu
* Fixes issue where while renaming a file, double clicking on word opened the file
* fixes issue where while dragging multiple items the top-most item was transparent resulting blurry mess
* when opening a folder, update view instantly
* Improved drag-to-select items
* Added feature where typing selects matching file items
Fixed issue where keyboard listener for file list was active when context menu was open
* Fixed issues with File -> New in context menu
Fixes issue where files-selection-actions menu didn't disappear after deleting items
* Improved drag to select behavior
Improved drag items
Improved worker badge handling
* Cancel zone for dragging items (when spring-loading subfolders)
* context menu's submenu tweak
* Fixed issue where resizing window didn't work (when using "Open in New Window" from context menu)
* Disabled buttons on .files-selection-actions while drag-to-select is happening
* Fixes issue where creating new items using right-clicking re-rendered the directory view
---------
Co-authored-by: Claude Opus 4.5 <[email protected]>
Co-authored-by: ProgrammerIn-wonderland <[email protected]>
Handle upgrade flow when running in the app environment by calling await puter.ui.requestUpgrade() in driverCall_. Added app-specific branches in two places where usage/insufficient-funds were previously handled only for web (which showed a web dialog). This ensures the in-app UI is used for upgrade requests and awaits the result.
Temporarily comment out code in UIWindowPublishWorker that updates item UI after publishing: hiding the lines that show the item website badge, show per-item website-url badges, and set the data-website_url attribute. This prevents the publish worker from automatically modifying those DOM elements/attributes (kept commented for potential rollback or debugging).
* add sandboxing by default in god mode workers
* closes#2481
* Update UIWindowPublishWorker.js
* Add sandbox option to worker publish UI
Add a collapsible 'Advanced' section with a 'Sandboxed' checkbox (checked by default) to UIWindowPublishWorker. On publish the code reads the checkbox state and builds createOptions ({ sandbox: true } or { sandbox: false }) and passes it to puter.workers.create as an argument so workers can be created in sandboxed or non-sandboxed mode. Small UI styling and markup for the details/summary block included.
---------
Co-authored-by: jelveh <[email protected]>
* [PUT-471] feat: estimate and bill input for gemini imgaes :dev:
* [PUT-472] add input/output costs to openai image models too
* npm version bump
* remove second url log
* fix gemini pro image
---------
Co-authored-by: ProgrammerIn-wonderland <[email protected]>
For reasons still unknown, there are circumstances where the permission
scanner denies permission to access tokens that should have been granted
permission. This change de-abstracts/flattens the logic for checking
access tokens so that it is easier to understand and maintain. The newly
implemented code does not suffer from the same issue.
Change default behavior for email confirmation by initializing email_confirmation_required to 0 (not required) instead of 1. Remove the duplicate declaration and clarify the comment; existing logic still overrides this default (sets to 0 for matching pseudo_user/uuid_user and to 1 when event.requires_email_confirmation). Minor whitespace cleanup.
After a successful flow, fetch the user app token for the opener origin, cache the returned app_uid on window.host_app_uid, and post a 'puter.token' message to the opener containing success, msg_id, token, username, and app_uid. If the action is absent or equals 'sign-in', close the popup window. This enables parent windows to receive credentials in popup/embedded auth flows.
It is possible this broke after redis caching was employed because it
broke the expectation that assignments to attributes on cached objects
would remain for future accesses on said objects. This has not been
confirmed as the cause.
This also causes the filename to have the extension `.mjs` instead of
`.js`, as well as an await added in FilesystemAPIService. Luckily
lifecycle events are async so this didn't cause any problems.
I add this change somewhat reluctantly because this _should_ be a
non-functional change. Technically adding the `await` in
FilesystemAPIService makes it possible (albiet incredibly unlikely) for
some subtle bug or change in behavior to be introduced.
This change makes GUI invoke readdir without fetching subdomains, and
then fetch subdomains after. This allows displaying the directory items
sooner and then later populating the icons with glyphs to indicate if
there is an associated subdomain.
This was AI-assisted with manual modifications and bug fixes as
necessary.
This endpoint can be called to get subdomain information after
performing a readdir without fetching subdomains.
This was AI-assisted with modifications and bug fixes as necessary.
This parameter will allow readdir requests to not include subdomain
fetching, which will allow for the later effort of fetching subdomains
for directory listings in the gui with a separate request.
This extensions brings back the dev-socket functionality, which is
really important when testing things like broadcast, alarms, events, etc
; it saves a lot of time if you can invoke a command directly to the
backend.
This is an optional extension that will not be included in production
deployments. This is for development purposes only.
This still needs to be tested. I was going to test this using the `test`
command registered by BroadcastService that exists for this purpose but
`dev.sock` doesn't seem to be working anymore.
Why wasn't `dev.sock` working? Well... we deleted all the code that
makes it work. Why did we delete it? That question isn't a setup for my
next statement; I genuinely do not know. The whole point of dev.sock was
so we could remove the dev console but still maintain support for this
commandline interface that's absolutely needed for testing things that
aren't accesible directly from Puter's GUI.
This tool makes it possible to manually test webhook support in
BroadcastService without running multiple Puter instances. This helps to
verify the functionality without setting up multiple Puter peers
locally.
This commit adds the "receive" side of webhook support for
BroadcastService, which will eventually make broadcasting stateless and
not requiring of persistent connections.
Some specific considerations taken into account include:
- incremental nonce to prevent replay attacks
- request timestamp to prevent nonce-reuse after restarting
- HMAC signature to ensure authorized peer
Known limitations:
- if instances run indefinitely, eventually the nonce value would wrap
around to zero and broadcasts would stop working. It is assumed that
9 quintillion requests in the lifetime of an instance is reasonably
impossible.
It turns out this part of `35461a0` was not necessary to fix this issue,
and the code is still more correct if it falls true when the token's
authorizor has a permission granted.
Range header support was mostly incorrect, which brought to surface
undefined behavior in OnlyOffice which resulted in a request hanging.
This was the cause of the `pdf-editor` app hanging for a while when
opening it with a new file.
When the user clicks Cancel in the authorization window, the code now disables action buttons and replaces the window body with a cancellation UI (header with icon, title, description and a message). Added corresponding English i18n keys (authorization_cancelled, authorization_cancelled_desc, authorization_cancelled_message) to provide localized text for the new cancelled state.
Introduce a new UIWindowAuthMe component that displays a security-focused consent dialog for redirecting to third-party URLs with the user's auth token. Adds related i18n strings and integrates the dialog into initgui, replacing the previous UIAlert confirm flows; when approved (resolves true) the token is appended to the redirect URL and navigation proceeds. Changes: adds src/gui/src/UI/UIWindowAuthMe.js, updates src/gui/src/i18n/translations/en.js, and imports/uses the new dialog in src/gui/src/initgui.js.
Introduce a new UIWindowCopyToken component to display the user's auth token with a copy-to-clipboard button and confirmation message. Integrate the token UI into the account dashboard (TabAccount) by adding an auth token card and click handler to open the window. Add corresponding i18n keys (auth_token, token_copied, copy_token_message, copy_token_description) and import the new window in initgui. Also wire the global action 'copyauth' to show the token dialog and adjust session/login flow so the session list/login windows respect the action (avoid auto-reload when showing the token dialog). Minor UI string cleanup to use i18n('approve') directly.
Replace the plain text response with a styled HTML confirmation page for the local authentication callback. The handler now sets Content-Type to text/html and returns a responsive, animated "Authentication Successful" page (with checkmark graphic and Puter branding) while preserving the existing token extraction from the callback URL's search params.
Read the 'action' value from URL query params and handle a new 'authme' action. When 'authme' is present, prompt the user to approve redirecting to the provided redirectURL; if approved, append the current auth token as a 'token' query parameter and navigate to that URL. The prompt is shown in two places (after auth data is updated and before loading the desktop) to cover different entry flows. Also removed a stray blank line.
There were a couple issues with ACL where access would be denied to app
tokens within access tokens, caused by:
- incorrect recursion: only `false` was considered when in fact the
result from the recursive call is appropriate
- files without a known path are incorrectly handled, which can happen
when iterating over parent directories
* docs: add example for streaming with function calling
* Minor fix and add example to playground
---------
Co-authored-by: Reynaldi Chernando <[email protected]>
WebDAVService catches `/*`, which shouldn't be a problem because it is
on the `dav.` subdomain - however, other middlewares were being invoked
before the subdomain check. Thanks to the `'route'` parameter for the
`next` function we can handle subdomain checks in a more reliable way
* Add ststem info to user options extensions - Add UIWindowSystemInfo, add ui sections for client and server, add basic getClientinfo function
* Fix typo
* Replace accidentally deleted es.js file
* Refactor client information to be consistant with project standard
* Complete Client information in ststem information window
* Remove console logs
* Add basic api functionality for getting server system information
* Structure return data from system server information endpoint | Add copyright to UIWindowSystemInfo
* Add function to format server system api data | Add loading element to server container while waiting for data | Complete System Information
* fix: disallow non admin for backend + move to extensions
---------
Co-authored-by: Daniel Salazar <[email protected]>
This is the effort remaining to bring back URL parameters to drivers
which support them. This fixes the implementation for DNS resolution
resulting in errors for undefined IP addresses when DNS resolution is
successful.
Introduced a resize_window_to_aspect_ratio function and added 'Advanced' menu items to allow resizing the window to 16:9, 4:3, or 9:16 aspect ratios. This enhances user control over window dimensions for specific use cases.
This reverts commit 2139551abf.
It looks like this breaks dev-center because the file associations
aren't properly sent. I will need to investigate this.
This is the first functional migration from ths `es:app` driver to the
`app` driver. Any call to `puter.apps.get` will go through `es:app`
after this change.
* wip: batch fetch get apps for launch apps and recommended apps
* fix: boot issues
* fix: bad refresh app cache
* tmp: remove test for now since can't mock db call
This is the first functional migration from ths `es:app` driver to the
`app` driver. Any call to `puter.apps.get` will go through `es:app`
after this change.
It turned out this error was not coming from `app`, but it was coming
from `app:es` due to a pre-existing issue. Since these tests are meant
to compare the resulting behavior and `app` can handle this case while
`app:es` cannot, the best course of action is to avoid invoking the
issue and only test the intended behavior.
Add tests for all the CRUD operations and related edge cases in `app` to
ensure the behavior is the same as `es:app`. These tests detect 3 issues
across 4 failing tests.
This is comitted separately from the commit for AppService.comp.test.js
that will follow because it interacts with all other test cases. It did
not cause any issue based on what I observed, but I'm still wrapping it
in a separate commit in case we need to revert or change it later.
An "anti-test" (test that should fail) was created to verify that a test
fails when apps are different. This revealed a problem in the original
logic of comparing the database state (UUIDs don't need to be, and will
not be, identical; they need to be excluded from comparison). This
commit contains the new test and fixes the existing test case, which was
passing because this problem caused a false-negative.
The file `context.js` causes problems in unit tests because vitest does
not play nice with CJS, especially when both CJS and ESM are in use such
as in our repository.
Given that it is not feasible to solve the underlying interoperability
issue, I have elected to make `context.js` a singleton in `Symbol.for`.
This worked very well with `helpers.js`.
The `helpers.js` file has two instances when testing with vitest.
Usually this can be solved by creating an "import bridge" file - another
CJS file that imports the desired CJS file and re-exports it so that the
`require` mechanism for caching modules is used instead of the `import`
mechanism for caching modules. Unfortunately in my most recent test
cases this does not work. I do not know why this does not work. However,
without doing a deep-dive into the internals of vitest I can at least
see that the only state in helpers is for providing `services` as a
pseudo-global, so a hack with `Symbol.for` solves everything.
This test was very difficult to get working because of issues
initializing the context under vitest. After a lot of effort and trying
to narrow down the issue in many ways that didn't work, I eventually
found that manually setting the storage and calling run on the
AsyncLocalStorage object got it working. I'm currently unsure if this is
specific to vitest or not. We don't seem to do this in Puter's kernel
but everything works fine in the typical runtime. Maybe this is
side-effect of otel's own use of AsyncLocalStorage.
This is a naive implementation of the `.upsert()` method that simply
calls `.create()` or `.update()` depending on whether or not the app
already exists.
Primarily these tests were AI generated an then checked over for sanity.
These tests are good unit tests to avoid regressions as a result of
changes to this code but are not sufficient to test for regressions
introduced by this new implementation that will replace `es:app`.
Fixes are also included for a couple issues that were found:
- `null` being included in extensions list
- property called `user_owner` instead of `owner`
Most of this implementation matched `.select()` which saved time. The
common behavior can be factored out, but since the purpose of this
refactor is to flatten everything as much as possible I'm going to leave
the duplicated code and make a note to do this later.
The challenging part was `#build_complex_id_where()`, which is the
equivalent to the `fetch_based_on_complex_id_()` method in
EntityStoreService. This allows `uid: app-UUID-OF-APP` and `id: { name:
'editor' }` to both work.
Adds the `icon_size` parameter to the `.select()` method on the new
non-ES/OM implementation.
When adding the necessary `params` parameter to the `.select()`
method, it was noticed that the `predicate` parameter did not have a
default value - potentially causing a regression; a default value was
set for both `params` and `predicate`.
It turns out self-referencing tables add some nuances to what is and
what isn't ambiguous in SQL queries, so getting this right took a little
longer than expected. Following the entire chain doesn't seem to be
possible so a compromise is made by only traversing one level of depth.
(you can the uid of the owner of the app, but not the uid of the owner
app's own owner)
The "flat" implementation of permissions was broken because
revokePermission tried to delete entries by calling `.set` with an
option called "delete". I'm not sure if this has ever existed on the
puter-kvstore interface, so it's weird that it was called like that.
The permission implicator for protected apps was written before changes
to the permission system that affect the conditions under which a user
is allowed to grant and revoke permissions; specifically this is the
`manage:` set of permissions, which now needs to be granted to the owner
of a protected app.
Additionally, the "level" component of the permission is ignored because
the owner of a protected all is implied to have all the permissions
pertaining to that protected app.
Make the home directory (AKA "~") the default CWD for puter.js, making
relative paths work in environments other than app environments (i.e.
gui or worker environments).
Handle a race condition affecting mkdir operations with the
`createMissingParents` option enabled. When creating a directory because
this option was specified, errors because the directory already exists
are likely due to a race condition and should not be propagated to the
client but instead handled by using the directory that's already there.
* Improve UI for App Deployment Options in Dev Center
- Added CSS styling for deployment option cards
- Implemented active state switching between deployment methods with guardrails
- Abstracted saveEditApp for reuse across Deploy and Edit tabs
- Added error handling for invalid URLs in the Deploy tab
* updated mode's description and fixed function's error
* updated by npm install
* Revert "updated by npm install"
This reverts commit 82cd962f62.
* in url mode the deploy button is enabled by default
* Add README for Puter localization and setup
This README provides an overview of Puter, including installation instructions, system requirements, and support information.
* Update README.od.md
---------
Co-authored-by: Nariman Jelveh <[email protected]>
Introduces thumbnail generation for image files during upload operations. Thumbnails are generated client-side if enabled via the `generateThumbnails` option, and are included in file metadata and operation payloads. This enhances file previews and user experience in the file system.
Based on a stack trace from production and manual static analysis, it
appears that a race condition occurs within hashing_stream
intermittently. The conditions that cause this issue might be heavy
backpressure. The source stream emits the 'end' event before
stream.Transform's transform callback has had a chance to process all of
the chunks, which results in an incorrect order in calling methods on
the `typeof crypto.createHash()` object.
It would appear that `source.pipe(stream)` is not able to propagate
errors from Transform because the chain was simply broken and upload
would get stuck (if this was indeed the cause of upload getting stuck,
which seems likely but not conclusive)
There was a configuration parameter for this but it isn't being used and
has the incorrect name in `config.js`. I decided to keep the old check
here anyway because having support to configure a lower size limit could
come in handy.
I set the limit to 2MiB through the following logic:
- largest icon size I've ever heard of is 512px
- 512*512*4 (4 color channels) is 1048576 (1MiB)
- increase to next MiB just to be safe
Deleted the ComplainAboutVersionsService and its registration in SelfHostedModule, as well as cleaned up related service mappings in DriverService. This streamlines the codebase by removing the version complaint feature.
Fixes#2218
The DEV Center was unable to open worker files in the text editor because
it lacked godmode permissions. The file_paths parameter in puter.ui.launchApp
requires the calling app to have godmode=1 in order to work.
Changes:
- Added database migration 0044_dev-center-godmode.sql to set godmode=1
for the dev-center app (uid: app-0b37f054-07d4-4627-8765-11bd23e889d4)
- Updated SqliteDatabaseAccessService.js to include the new migration
This allows DEV Center to successfully pass file paths when launching the
editor app, fixing the issue where clicking on a worker file path resulted
in an empty editor window.
There were two different things here:
- invalid requests have an ambiguous error message
- migration of PuterFSProvider to an extension caused an issue
for shortcut creation in some flows
Closes#2200
Remove a method in hl_mkdir which was determined to not be referred to
anywhere. hl_mkdir was re-tested to verify that this did not cause a
regression.
This error message was ambiguous for mkdir operations where a shortcut
target is specified for hl_mkdir. The message looks like it's referring
to the shortcut target but really it is referring to the parent
directory for the new directory that will be created.
This merits a test because changes to how `puter.site` works in the
future could easily break this redirection, and maintaining this
redirect is important to us.
Redirect the subdomains `""` and `"www"` to a configurable URL, which is
by default `https://developer.puter.com/static-hosting/`.
This is potentially a breaking change for local instances that have a
subdomain called "www". Users hosting their own Puter may want to be
aware of this.
Closes#2214
Removed unnecessary console.log statements from PuterFSProvider.js and WebServerService.js. Increased PENDING_QUERY_TTL to 3 seconds and added a timeout mechanism for coalesced queries in get_app to prevent indefinite waiting.
* Update first-run-issues.md with dnf5 instructions
Add installation instructions for Development Tools on dnf5.
* Fixing the command to install it on Fedora 41+
Introduces a TTL-based pending query cache to the get_app function to prevent duplicate database queries for the same app lookup. This change improves efficiency by coalescing concurrent requests for the same app and ensures only one database query is performed per unique lookup within the TTL window.
This commit adds the DataAccessModule, which is where replacement
"flatter" implementations for what is currently implemented via
EntityStorage/OM will be developed.
This commit also adds AppService, which is where the `crud-q`
implementor for apps will be implemented.
This commit adds the `select()` implementation for AppService. Currently
it is missing many behaviors for parity with the current ES/OM
implementation including:
- coercion of boolean values
- nested objects and their properties
- `icon_size` parameter support
The EntityStorage/OM mechanism for data access is being replaced.
Specifying 'es:app' in puter.js calls will allow the ES/OM mechanism and
its replacement to be tested together to check for regressions. When the
replacement is done, we can change 'es:app' to the new service name.
This is comitted separately because it should not have any functional
changes. Do not skip this in a `git bisect`, but do not revert this when
testing commits for functional changes.
Eliminated the spinner shown when creating a temporary user in popup mode and removed the corresponding hideSpinner call. This streamlines the user experience during account setup.
Adds the necessary configuration and the `npm run bench` script, as well
as the first benchmark to verify everything is working. The first
benchmark is permissionUtils. This is important to benchmark because
permission strings are used often and there is some non-trivial logic
such as escaping of full-colon characters. Other overheads like database
access will be much more significant, but it may be useful to know how
much more significant in quantifiable terms.
It took a long time to figure out why this was happening, but
user-to-app permissions and dev-to-app permissions were being checked in
sequence instead of concurrently. It turns out it was a recursive call
in user-to-app permissions that was blocking. This recursive call was
not actually a dependency of the query for user-to-app permissions; it
was only needed for determining if default user-to-app permissions are
held by the user. This was fixed by simply splitting up these
non-dependent queries into two separate scanners.
* Remove smol utility library and update usage
Deleted the smol utility library from putility, removed its documentation, and refactored code in linux.js to use native string methods instead of smol.split. This simplifies the codebase by eliminating a redundant utility.
* Remove `putility.libs.time` and inline time utilities
* Update imports and time constant usage in core modules
Replaced usage of time.SECOND with a local SECOND constant in ServerHealthService for clarity and removed unused import. Updated all service imports in TestCoreModule to explicitly include the .js extension for compatibility with ES module resolution.
* Remove unused `wrap_text` and `ansi_visible_length` functions
Deleted the wrap_text and ansi_visible_length utility functions from string.js, as they are no longer needed or used in the codebase.
* Remove `format_as_usd` utility and related docs
The format_as_usd function and its documentation have been removed from the codebase and README files. This cleans up unused or deprecated currency formatting utilities.
* Remove osclink utility and update references
The osclink function and its documentation have been removed from putility. All usages in backend modules have been updated to use plain URLs instead of OSC 8 hyperlinks. This simplifies string utilities and avoids terminal-specific formatting.
* Remove PosixError utility module
* Remove NariMethodsFeature and related references
Deleted the NariMethodsFeature module and removed its usage from AdvancedBase and PuterJSFileSystemModule. This simplifies the codebase by eliminating unused or deprecated NARI method support.
* Remove DevConsoleService and related dev console widgets
Eliminates `DevConsoleService` and `NullDevConsoleService`, along with all code that registers or manages dev console widgets across the backend. Developer messages and notifications are now logged directly to the console. Removes related commands and test mocks, simplifying the developer experience and reducing code complexity.
* Remove MinLogService and related registration code
Deleted MinLogService.js and removed its registration from SelfHostedModule.js. This cleans up unused or deprecated logging functionality.
* Remove `DevSocketService` and related registration
* Increase test job timeouts in CI workflow
* Remove dev environment widget ops counter
Eliminated increment and decrement of the dev_batch-widget ops counter in BatchExecutor. This simplifies the code and removes dev-only UI tracking logic from the batch operation flow.
* Remove console Puter logo feature and related code
Deleted the src/fun/logos.js file and removed all references to the Puter logo display in WebServerService and documentation. This streamlines the server startup output and eliminates unused or unnecessary code.
* Update WebServerService.js
* Standardize console banner formatting in services
* Remove tag cache refresh logic from AppInformationService
Eliminated the _refresh_tags method and its scheduled interval from AppInformationService. This change removes the tag cache refresh functionality, likely due to refactoring or deprecation of tag-based app discovery.
* Remove AI usage testing contributor docs
* Remove dev-console-ui-utils and update related services
* Remove debug console.log statements from backend code
Eliminated various leftover console.log and debug print statements across multiple backend modules and services to clean up the codebase and reduce unnecessary logging in production.
* Remove unnecessary console.log statements
Deleted the CommentService implementation and its test file, and unregistered the service from CoreModule. This removes all comment-related backend functionality.
* Refactor puter-js to remove putility and service layer
* Add fallback for IPC listener registration
* Remove TeePromise in favor of `createDeferred` pattern
* Update utils.js
* Bump version to 2.2.0 in package.json
* Remove model name normalization and driver mapping logic
Eliminated the code responsible for normalizing model names and mapping models to specific AI drivers. The default driver is now set to 'ai-chat', simplifying the model selection and driver assignment process.
* Simplify AIService selection logic in image generation
* Remove unused Together image model constants
* Remove terminal and phoenix built-in apps and emulator
This commit removes the terminal and phoenix built-in apps from the backend, database migrations, and permissions. It also deletes the entire emulator and phoenix/terminal app source directories, assets, and related configuration files. References to these apps in the backend module loader and router are removed, and related SQL migrations are updated to reflect their removal.
* Remove emulator references from SelfHostedModule
* Remove emulator integration and related references
* Remove parsers and pty modules
* Update package-lock.json
Deleted ThirdPartyDriversModule.js, html_footer.js, and html_head.js from the backend source directory. These files are no longer needed and have been removed to clean up the codebase.
Deleted the src/backend-core-0 package and moved its validation helpers directly into src/backend/src/validation.js. Updated references in CoreModule.js and helpers.js to use the new local validation module. Adjusted ESLint configs to remove backend-core-0 references.
* Revert "fix: together ai image prices (#2130)"
This reverts commit 04c8b06616.
* Revert "feat: single image ai endpoint (#2129)"
This reverts commit 161ca74aa3.
* Revert "fix: prevent loot qp if failing ai models? (#2122)"
This reverts commit 8d52cefe47.
* Revert "feat: rename ai models to match norm (#2121)"
This reverts commit 9da8aa0849.
* feat: refactor ai to have single entry point and follow defined model structure
* fix: missing openrouter registration
* fix: dedupe models
* feat: provide usage in stream mode 🚀
We weren't able to see any custom spans, only the builtin
instrumentations for node.js. This commit downgrades opentelemetry so
that we can see custom spans in tools like jaeger as before.
Previously a new ConsoleSpanExporter was passed to the constructor of
NodeSDK. This was possibly unintentional but it did not cause any
issues. After the recent update to opentelemetry, or perhaps the changes
introduced in 3c579dcc0d, this no longer
works as it prevents tools like Jaeger from getting any spans from
Puter's backend.
* Add dashboard UI and routing support
Introduces a new Dashboard UI component with sidebar navigation and user options. Updates backend routing to redirect /dashboard to the root path. Integrates dashboard mode detection and initialization in the GUI, including responsive styles and logic to open the dashboard or desktop as appropriate.
* Improve dashboard user menu and UI behavior
Added support for multiple logged-in users, session saving for temporary users, and improved context menu options in the dashboard. Updated CSS for user button state and adjusted font size for signup terms. Changed dashboard initialization to use UIDashboard instead of UIWindow.
* Add dynamic apps section to dashboard UI
* Add specific class for dashboard apps section
* Refactor dashboard tabs into modular components
* Update style.css
* Add Developers menu and improve signup window behavior
Added a 'Developers' menu item to the dashboard linking to developer.puter.com. Updated login and signup window logic to ensure the signup window opens centered and dominant, improving user experience when transitioning from login to signup.
* Update UIWindowLogin.js
* Refactor apps tab UI and improve app card interaction
It calls requestPermission on the UI module. The method on the UI model
needs to still exist because the UI module is tightly-coupled with IPC.
A refactor of the IPC mechanism on the puter.js side could solve this.
This commit adds methods to request standard directories like the
Desktop, Documents, Pictures, or Videos directory. Additionally, the
message displayed when permission for these directories is requested is
easier to read.
This bug took a long time to diagnose, so I also made an async/await
wrapper around #postMessageWithCallback called #postMessageAsync so that
similar promise resolving errors are less likely here in the future.
The vitest test suite can import different states of the same module, so
this value was very helpful in determining that this was happening and
eventually the cause.
Permission endpoints would trigger 500 errors in cases where the request
did not have correct types for values in the request body. This migrates
all of these endpoints to use the new `validate_fields` function, which
is intended to make validation of fields clearer and more consistent.
This commit also adds a new check to verify that the app which owns the
entity being edited matches. Previously this was not necessary because a
read on the same entity would have always performed the same check and
caused this operation to stop early. (now that an app may have
permission to read entities created by other apps, this is no longer the
case.)
...entity (app, subdomain, etc). When trying to update some non-existing
entity the error message can state that an entity with a particular UID
was not found even if a UID wasn't specified. The UID reported will in
fact be a randomly generated UID because of how default properties work
in entities. This fix makes the error message more specific about which
identifier was used when an entity was not found. This is accomplished
by printing the predicate in a human-readable format.
This update fixes configuration issues in the TypeScript setup by standardizing the tsconfig.json. It sets modern compiler targets (ES2024), uses Node16 module resolution, enables strict type checking, and cleans up test-related exclusions. These changes improve build reliability, prevent casing inconsistencies, and ensure accurate source mapping during development.
By default node has a global `crypto` variable conforming to the
standard for the Web Crypto API. However, this `crypto` object does not
have the `createHash` function, so 'node:crypto' must be imported
instead.
* docs: add Punjabi translation link to README
The Punjabi (ਪੰਜਾਬੀ) translation README.pa.md was added 2 weeks ago but was missing from the translations list in the main README. This adds it to maintain completeness of the translations section.
* Fix Punjabi link formatting in README.md
---------
Co-authored-by: Nariman Jelveh <[email protected]>
* WIP Gemini OpenAI refactor
* refactor: gemini open ai service + geminiCostMap
* Gemini Service fixes
* Cleaning up old Gemini cruft
---------
Co-authored-by: Daniel Salazar <[email protected]>
* Add ElevenLabs TTS provider integration
Adds ElevenLabs text-to-speech support in the backend and frontend. Adds `ElevenLabsTTSService`, updates `PuterAIModule` to register the service, documents configuration, and integrates cost tracking with zero cost in `MeteringService` for now (todo). updates `AI.js` to support 11labs as a provider and adds related tests for `txt2speech` functionality.
* Update 11labs cost map values
* fix: debounce toolbar display after dragging a window
* change naming convention for debounce to match others. (snake case)
* revert changes to package.json that werent meant to be comitted
* Revert "change naming convention for debounce to match others. (snake case)"
This reverts commit aaedee0dfa.
* revert commit aaedee0dfa
* use snake case for drag_release_debounce_timer
This commit adds verification for statically identifiable definite
errors. Sometimes global variables used will be falsely reported as
undefined variables in which case the new file
`mandatory.eslint.config.js` should be updated to include these
definitions in the narrowest scope possible that has these definitions
This is the last error for the mandatory eslint rules. Now we can put a
rule into place so that CI always fails when there's an undefined
variable, which I think is pretty cool.
There's a potential issue here because of an undefined reference, but I
can't seem to reproduce any problem caused by this. However without this
fix this line of code is guarenteed to throw an exception so this fix
should be safe.
This is an error in a handler for a failure case so it shouldn't be
affecting users, but let's handle this error gracefully.
This was a symbol that became not defined after moving PuterFSProvider
to the extension.
LocalDiskStorageController was dependent on putility for TeePromise, but
since putility is a workspace module it's difficult for extensions to
access it (Puter backend core would need to expose it). Since TeePromise
itself is very small and will rarely (if ever) be changed I decided to
move it to a separate package on npm.
Begin work on LocalDiskStorageController in the `puterfs` extension.
This replaces LocalDiskStorageStrategy and LocalDiskStorageService from
the core. The `upload()` method is implemented to verify that it's
working.
This commit by itself will break other storage strategies. The next step
is to allow extensions to register storage controllers for puterfs. Part
of that work is done in this commit by emitting an event to register
storage controllers, but this commit does not include a way to
configure/select storage controllers.
Adding the hasChild method of FSNodeContext as well as a corresponding
method to filesystem providers is prerequisite to moving
FSEntryFetcher's logic into the new puterfs extension.
...use readdir from the provider instead of calling
fast_get_direct_descendants directly on fsEntryService.
This change is prerequisite to removing FSEntryService from core.
The get_capabilities and update_thumbnail methods in PuterFSProvider are
moved to the extension by this commit. This is prerequisite to the
removal of PuterFSProvider in the core.
Now that most of the functionality from PuterFSProvider in core has been
moved to PuterFSProvider in the `puterfs` extension, there are a lot of
unused symbols which need to be cleaned up.
This cleanup reveals that `update_thumbnail` and `get_capabilities` are
the only remaining functions left before PuterFSProvider in core can be
fully removed.
Introduces Mistral OCR integration in backend and updates the `AIInterfaceService` and `MistralAIService` to support new OCR options and response normalization. Updates metering cost map for OCR and annotation usage. Refactors `DriverService` to support interface-specific service aliases. Expands the puter.js `AI.img2txt` API to support flexible options and provider selection, including Mistral OCR.
FileCacheService was still a legacy service extending AdvancedBase. This
commit changes it to extend BaseService and implement the lifecycle
methods consistent with other services. This is a prerequisite for
moving write_overwrite which needs to import the service via
`extension.import` (not possible for legacy services) so that it may
invalidate file cache when a file is written to.
We want this to be fast enough that this benchmark doesn't take a long
time, but part of the effort of making it faster will be running the
benchmark repeatedly, and if it takes a long time then it will take
longer for us to make it faster.
This commit reduces the benchmark size for stat_intensive and
write_intensive so that they don't take several minutes to complete.
These two issues have affected us, but go away without rollup:
- https://github.com/npm/cli/issues/4828
- https://github.com/rollup/rollup/issues/6168
We can just use webpack instead; it has never caused build/npm issues.
This migration was done using AI, by pasting error messages over and
over again until everything worked. Since we're not actively working on
terminal and phoenix (dependents of rollup) currently it doesn't make
sense to focus on doing this migration manually, and any errors not
observed in basic use can be remedied later.
- Added a full French translation of the main README.
- Preserved all formatting, links, code blocks, and HTML elements.
- Ensures feature parity with the English version for French-speaking users.
* feat: comprehensive settings and modal UI/UX improvements
Redesigned settings interface and modal windows with improved visual
hierarchy, spacing, and modern styling throughout the application.
Settings UI improvements:
- Enhanced section headers with proper flexbox layout and centering
- Simplified storage usage display to single percentage with clearer info
- Improved settings cards with consistent spacing and hover states
- Redesigned session manager with horizontal card layout
- Reordered account settings (username → email → password)
- Removed duplicate password option from security tab
Form and input improvements:
- Increased container padding (20px → 24px) for better breathing room
- Tightened label-to-input gap (8px → 6px) for better visual grouping
- Enhanced labels with bolder weight (500 → 600) and refined color
- Larger input fields (12px 14px padding) with smoother corners (6px)
- Added hover states with border color transitions
- Removed distracting box-shadow focus rings across all inputs/selects
Button improvements:
- Modernized primary buttons with solid colors (removed gradients)
- Added flexbox centering for consistent text alignment
- Reduced button text size (13px) for cleaner appearance
- Improved default button styling with refined gray palette
- Better active/hover state transitions
Select dropdown improvements:
- Custom SVG chevron arrow with proper 12px right spacing
- Fixed select resizing issue on focus (removed global border/padding override)
- Full-width selects in modals with consistent styling
- Smooth hover states without size changes
Modal windows:
- Consistent 380px width for form modals, 400px for desktop bg settings
- Unified .settings-form-container class across all modals
- Proper ARIA labels and semantic HTML structure
- Error/success messages with refined colors and better contrast
Desktop background settings:
- Color blocks increased to 32px for easier interaction
- Fixed focus outline clipping with proper overflow handling
- Removed scale effects for cleaner interactions
- Flexbox layout for color grid with proper spacing
- Fixed palette icon display with inline background image
Window chrome:
- Increased default shadow (0 12px 24px, 0.12 opacity)
- Enhanced active window shadow (0 16px 40px, 0.18 opacity)
- Better visual depth and window hierarchy
File organization:
- Moved UIWindowManageSessions to Settings folder
- Created helpers/build_settings_card.js for reusable components
- Updated all imports to reflect new structure
* bring back shadow
* improve 2fa + fix theme not changing on cancel bug + fix couple more issues
* style polish
* remove unused styles
* i18n
* fix search bar clipping
* group reset & customize color buttons
* more polish
* enforce min width/height on resizable
* resizable settings and show full name on hover
* style tweaks
When ll_mkdir functionality was moved to PuterFSProvider, ACL and FSLock
concerns were erroneously moved into PuterFSProvider. The intended
design has ll_mkdir responsible for ACL and FSLock, and providers should
never be responsible for ACL.
An important note: it was noticed during this change that mkdir in
PuterFSProvider implements its own ACL check, instead of the ACL check
being performed in ll_mkdir. This means permissions won't be checked for
other implementors of mkdir! This needs to be fixed before custom
filesystem implementations can be considered production-ready.
According to the package documentation: https://www.npmjs.com/package/mime
> null is returned in cases where an extension is not detected or recognized
This call to `.startsWith()` was causing an exception to be thrown upon
right-clicking files without an extension, making it impossible to fix
them.
The styleguide for Puter's backend expects spaces after top-level not
operators inside conditions. This commit adds an AI-generated eslint
plugin and, since it conflicts with the existing space-unary-ops plugin
from stylistic, also adds a wrapped version of space-unary-ops that
ignores top-level not operators so that this plugin will work as
expected.
This service was registered within FilesystemService for legacy reasons
but now it needs to be accessed by an extension. In order for the
extension import to work as expected this service needs to be registered
at the same phase as other services.
This fixes PD incident Q3ZNFWILVOFKU3. This issue was not caught in
testing - in fact after deleting a user the files were deleted as
expected, but it's unclear to me how I observed that with this issue
present.
This temporary proxy provider will allow methods of the puterfs
filesystem type to be moved to an extension incrementally instead of all
at once. See comment above the class for a more detailed explanation.
There is a case where this.entry.id has a value in FSNodeContext while
this.mysql_id does not. I was under the impression that this state was
not possibole. The cause of this eludes me. This commit does not correct
the root cause - it only adds a workaround to this issue by falling back
to this.entry.id if this.mysql_id is `undefined`.
It might not be okay to assume an fsentry was already fetched when
updating the accessed time in ll_read. I'd like to know what changed
this to make this be the case, if it happens to be the case.
There were two references to `storage` in file.js but the search string
I used only detected one of them. I was looking for more references to
`storage` when I found this one. Also, I realized the metering updates
in file.js become redundant if ll_read is used instead so I've removed
those as well.
Adds an appropriate error at a couple different levels. The first is in
FilesystemService. Callers of FilesystemService.node are expected to
provide either an absolute path or an instance of a filesystem node
selector, but there wasn't an explicit check for this so instead an
arbitrary error (read property of undefined) would be thrown. The second
is a higher-level APIError in the batch executor that's a bit more
helpful than a 500 Internal Server Error.
This commit reduces the indentation level and complex arithmetic inside
the createTempUser function in initgui. It is suspected that this change
should greatly reduce measures of the "cognitive complexity" metric.
Apparently the callback operand of $.fn.fadeOut may be called more than
once if there are multiple matching elements. Although we expect there
to only be one element matching the selector `".captcha-modal"`, someone
editing the CSS and HTML would not expect such consequences to the
logic of captcha.
Calling `.fadeOut()` on the result of a jquery selector (i.e. the
evaluation of `$('.some-element-class')`, when it returns an object with
`length=0`) is a NOOP. That's right, it doesn't throw an error; it just
does nothing. This was preventing a temporary user from being created
when `.captcha-modal` isn't present because [the code intended to execute
after the animation] was never run. (square brackets for clarity because
the English is inherently ambiguous)
The PR I merged before putting this one up had my changes to api.d.ts
which I thought where on this branch - turns out I only thought that
because this branch had that one as a parent.
However #1818 has a simplification that this change overwrites. I'd like
to keep this simplification so this commit is restoring it.
The previous implementation of deleteUser was hard-coded to use the
PuterFSProvider file system type's storage implementation specifically,
even if a different filesystem type is mounted as root or for a
particular user.
This change was tested by creating a user, adding some fsentries, and
then deleting that user. The following classes of filesystem entry were
checked:
- [x] user created directory (with contents)
- [x] directory fsentry was deleted
- [x] user created directory (empty)
- [x] directory fsentry was deleted
- [x] immutable directory
- [x] directory fsentry was deleted
- [x] user created file
- [x] file fsentry was deleted
- [x] file storage was deleted
- [x] user created file in trash
- [x] file fsentry was deleted
- [x] file storage was deleted
Accessing the storage object directly duplicates effort of locating the
correct filesystem provider based on the mountpoint of the file with the
contents being requested; an effort that file.js did not perform which
made it incompatible with filesystem mountpoints.
This change has been made to make it easier to move filesystem type
implementations to extensions, because accessing storage directly will
not be supported after puterfs is an extension.
- A new stt driver in `AIInterfaceService` and `PuterAIModule`.
- added methods for audio transcription and translation in the speech-to-text interface.
- updated cost mapping for stt models in `openAiCostMap.ts`.
- Updated permissions and interfaces to support new speech-to-text features.
- added support for OpenAI as a tts provider in `AIInterfaceService` and `PuterAIModule`.
- Updated cost mapping for OpenAI tts models in `openAiCostMap.ts`.
- improved `Txt2SpeechOptions` interface to include new parameters for provider, model, and response format.
- added tests for OpenAI provider in `txt2speech.test.js` to ensure functionality.
This extension has a copy of memoryfs which is exposed as `testfs`. The
purpose of this is to register a new filesystem type from an extension
to ensure it works as expected and to get feedback on a working example.
* Don't update URL when opening public files via direct URL
* Alert user when attempting to open a nonexistent file
* Fix toolbar autohide in public file urls
* Remove extraneous debug logs
* Properly define stat var in public folder URL handling
* Update window URL when focusing an app opened through file URL
* Localize error messages
* Update OpenAI package to version 6.7.0 and add video generation capabilities
- Updated `openai` dependency in `package-lock.json` and `package.json`.
- Introduced new permissions for video generation in `hardcoded-permissions.js`.
- Added video generation service in `AIInterfaceService.js` and registered it in `PuterAIModule.js`.
- Implemented coercion for video streams in `CoercionService.js`.
- Updated driver service to include video generation.
- Added `txt2video` method and options in `AI.js` and corresponding TypeScript definitions in `index.d.ts`.
* Update openaiVideoCostMap.ts
* Revert "Update openaiVideoCostMap.ts"
This reverts commit 9e66f93605.
* change `txt2video` to `txt2vid`
Instead of switching to test mode, which responds with "Lorem Ipsum"
sample output to test the API, throw an appropriate error message for
content moderation failures. This is especially useful when OpenAI's
moderation is way way way way too aggressive as this confuses users.
I was not able to locate a change causing a configuration regression,
however I do know AI was working in my OSS clone and that I had no
reason to change my AI configuration in my `local.json` file. In any
case, it is clear that configuration was not working as intended.
This commit fixes PuterAIModule which was not enabling OpenAI services
when `openai` was configured within the `services` block, even though
the `services` block configuration is supposed to be the recommended
configuration and the global `openai` config object is supposed to be
deprecated.
This commit also fixes OpenAIImageGenerationService which was not
accepting the current parameter name for the API key (`apiKey`) and
instead only accepted the legacy key (`secret_key`) which created this
interesting problem where using the recommended parameter name would
prevent Puter from booting successfully.
This commit begins the plan towards moving PuterFS to an extension. It
is first necessary to analyze the dependencies and determine which
services and modules are part of PuterFS and which ones are not.
Added a constant for PANEL_WIDTH to standardize panel width across the application. Refactored UIWindow logic to utilize this constant for positioning and resizing, ensuring consistent behavior when panels are open. Restored the getSnapDimensions function to adjust available dimensions based on the taskbar position and open panels.
In the prod environment there is no instance of DevConsoleService. If
methods are called on the service without a check to see if it exists,
this results in "read on undefined" errors that can only be produced in
a prod environment. This was the cause of the issue caused by the
previous log cleanup PR where it looked like "websockets were broken".
The missing undefined check is fixed by #1734, but the change in this
commit will catch future cases.
Additionally, this change will help with moving away from the dev
console.
Often fsentry stats are used to see if an fsentry exists, so fsentry not
found logs are common and expected. This commit disables those by
default. Additionally, logging for cache events is disabled as well,
since those are only helpful when debugging cache specifically.
* Implement detailed usage report table
- Added `number_format` function for flexible number formatting with options for decimals, separators, and negative formatting.
- Updated `UITabUsage.js` to include a toggle for viewing driver usage details, fetching and displaying usage data dynamically.
- Improved CSS styles for driver usage details to improve layout and interactivity.
* Update style.css
* Refactor driver usage display in UITabUsage.js
- Updated UITabUsage.js to improve the layout of driver usage information, including a new header structure for better organization.
- Added CSS styles for the new header layout and adjusted existing styles for improved visual consistency and interactivity.
* fix progress bar logic
* fix: adjust toolbar positioning for sidepanel based on taskbar location
* fix: update window width calculation for left position based on open panels
* chore: add http-server as a dev dependency and update package scripts
* refactor: move maximized window positioning logic to UIWindow and remove from UITaskbar
Removes init/construct logs, moves many boot logs to debug level,
introduces debug level in BootLogger, moves "CONFIGURATION MUTATED AT
RUNTIME" message to debug level and allows config.js to output logs to
the standard logger in context.
- Adjust `UIPopover` positioning when a sidepanel is visible.
- Update `UIWindow` to manage visibility states and adjust toolbar positions accordingly.
- Modify CSS for AI Button to change z-index for better layering.
* perf: move user-user perm checks to flat kv entries
* feat: manage permission for user to user and dev to app file sharing
* fix typings cuz macos sucks
* fix: mac os typecase
* fix: chore macOs typecase
* fix: bad join of permissions
* feat: add check-permissions endpoint for checking an users perms
* Add 'manage' to permission lists in the Sharing dialog
* fix: manage being allowed through our ACL
---------
Co-authored-by: Nariman Jelveh <[email protected]>
* add AI side panel to UIDesktop
* integrated `UIAIChat` component into `UIDesktop` for AI interactions.
* added styles for AI side panel, including buttons and chat message formatting in style.css, enhancing user experience with AI features.
* add support for window-based side panels
* remove window resizing logic that is simply broken
* whitelisting added
* Add `showWindow` and `hideWindow` methods to Puter.js
When /user-protected/change-password (and presumably other endpoints
under /user-protected) receive a preflight request they respond with
HTTP status 401. This is because `.use()` calls on the router apply to
all request methods erroneously.
This commit fixes the issue. Further investigation is required to
determine when preflight requests started happening on this endpoint.
* feat(Translation): added support for Armenian and also added Bulgarian
* fixed
* add license header to bg.js
* Update package-lock.json
---------
Co-authored-by: jelveh <[email protected]>
* improve UIItem and UIWindow to support internationalization for system directory names. Updated UIItem to display localized names for trash, desktop, home, documents, pictures, videos, and public directories. Modified UIWindow to set the window title using localized names for the desktop and home paths.
* refactor UIWindow to use i18n for system directory titles. Updated window title settings for desktop, documents, pictures, videos, public, and trash paths to use localized names, improving user experience across different languages.
* add option to automatically set window title to opened file's name
A new checkbox in the app edit section to allow users to automatically set the window title to the name of the opened file. Updated related functions to handle this new setting, ensuring it is saved and reset correctly.
* Update launch_app.js
Using services such as `kv` which expect an actor in context would not
work in a handler for a lifecycle event such as `'init`' in an
extension. In core backend modules is it generally expected that if a
user actor is not in context a backend service will explicitly specify
that the system actor should be used with the `su` service's `sudo`
method. We cannot require extension developers to add this wrapping code
whenever they want to use kv.
In the future it might be better to have a specific "ExtensionActorType"
for this purpose.
The basic `.get()` and `.set()` calls should be the same between
persistent key-value store (puter-kvstore) and in-memory cache (kv.js).
This commit wraps these methods to support the "shorthand" usage.
The `/drivers/call` endpoint hangs when the JSON middleware produces
(or, is going to produce?) a PayloadTooLargeError. Increasing the limit
avoids this issue for typical use.
The extension documentation mentions imports and exports together very
early on, but extension exports should be used sparingly. I suspect most
extensions with exports will be among Puter's builtin extensions.
Extensions now read configuration objects under the "extensions" block
in the backend configuration. Additionally, a `config.json` for default
values directly in the extension's directory is now supported.
* Add 'Set as Desktop Background' context menu item
* Use the `mime` library to detect images in the `UIItem` context menu
---------
Co-authored-by: Nariman Jelveh <[email protected]>
Removes dead code and reduces branching. In the get_service_or_throw_
method, the changes in this commit show how the branching got more
redundnat in the previous change.
Adds new events that DriverService emits: `create.interfaces` and
`create.drivers`. Returns the functionality of the "drivers" registry,
which now expects objects provided by extensions.
- Implemented `tarItems` function to create tar archives from selected files or directories.
- Added `untarItem` function to extract tar archives.
- Updated UI to include options for downloading and tarring files.
- Enhanced item icon handling to display tar file icons.
- Added translations for tar and untar operations.
- Fixes#1629
The Anthropic SDK preemptively throws an error if it thinks a response
is going to take longer than 10 minutes:
https://github.com/anthropics/anthropic-sdk-typescript#long-requests
According to this documentation, overriding the `timeout` option
disables this error. In this commit the option is changed to the default
value of 10 minutes plus an additional 1 second; it is assumed based on
the documentation's phrasing that it is not necessary to make the
timeout higher but simply to specify a value.
RuntimeModules (a feature for extensions) can be accessed by core
modules through a registry exposed in Context by Kernel. However,
TestKernel was not exposing this same registry which caused an error
when module installation occurred during tests.
Updated the `bindSocketEvents` method to check the `original_client_socket_id` before posting updates for 'cache.updated' and 'item.renamed' events. This prevents unnecessary updates when the event originates from the same client socket.
Updated the `WSPushService` to pass additional metadata during user timestamp updates and event emissions for file system operations. This includes modifications to the `_on_fs_create`, `_on_fs_update`, `_on_fs_move`, and `_on_fs_pending` methods to ensure metadata is consistently included, improving the context for GUI updates.
We do update the path of all child directories by calling
filesystem.update_child_paths(), but this was never done for the user's
actual home directory itself (only everything under it).
The most ideal solution would be to delegate this behavior to the move
operation instead of updating the fsentries table directly, but making
this change right now has a high risk of breaking changes since this the
behavior in this file is very old.
The data extension adds convenient access to the database and kvstore
via it's 'db' and 'kv' exports.
Example usage:
const { db, kv } = extensions.import('data');
await kv.set('some-key', 'some-value');
await db.write('INSERT INTO something (a, b) VALUES (1, 2)');
This commit adds the `extension:list` command for listing extensions.
This command is itself defined in an extension. To make this possible,
an event called 'create.commands' is emitted form CommandService with
the event object holding a function that can be used to add commands.
The custom eslint pluggin for the formatting rule I have for the
expressions in control structures isn't a good fit when the condition is
a single variable named by a single character. This happens a lot with
`catch`. `catch ( e ) {` has too much spacing, but `catch (e) {` looks
relatively normal.
Extensions need to be documented at different layers of concern:
1. How to use/run extensions
2. How to create extensions as a contributor
3. How extensions work under the hood
This documentation addresses the third layer, describing how Kernel
and core modules interact with extensions.
I thought you could:
method() {}
set method (v) { /* ... /* }
but you can't. You have to:
get method() { return (function () {}).bind(this) }
set method (v) { /* ... /* }
I don't know why a setter can't just only shadow setting, but that's
how they designed the language. ¯\_(ツ)_/¯
This change makes sure Kernel skips `.git` directories when scanning
for extensions. This allows me to make a git repository containing
example extensions for Puter.
Previously '{source}' was added but this is a bit difficult to work
with since it's the path from the source file, which is usually the
'src/' directory under the repository. This won't be obvious anyone
who doesn't already know this. '{repo}' just points to the path of
the repository itself.
Extension developers should not have to remember what comes form
where between two different globals. This change makes Extension
effectively a facade (Facade Pattern) containing the behaviors
of both extensions and runtime modules.
RuntimeModule is a new construct which represents the import/export
mechanism for Puter extensions. This can be thought of as analogous
to node's Module class. These are called "runtime modules" because
extensions have less control of what they're consuming than a typical
npm module; i.e. `runtime.import('database')` will provide a version
of 'database' that's compatible with the importing extension, but the
version used can't be locked or otherwise determined by the extension.
* client-cache: remove pulling
* client-cache: fix wrong ts field
* client-cache: purge cache on any local update
* client-cache: last_updated_time -> last_valid_time
* client-cache: update cache on remote update
* client-cache: update last_valid_ts every sec
* client-cache: switch to localstorage
* do the cache and purge test when `setAuthToken` is called
---------
Co-authored-by: Nariman Jelveh <[email protected]>
This commit fixes the block that's meant to match backend files. There
was an error in the arguments for '@stylistic/indent' which was
effectively disabling the entire block. In vscode, it is very unclear
that this is happening, so I had to manually run this command:
npx eslint "src/backend/**/*.js" --debug
This commit also disables @stylistic/quotes; I disagree with this
rule for several reasons:
1. Sometimes the appropriate quote depends on the contents:
i.e. '""""' + "''''" is cleaner than '""""\'\'\'\''.
2. If you argue that mixing quotes between concatenated strings
(like the previous example) is a bad idea, then I can further
state that this rule forces me to mix quotes:
`we use a ${templateString}` + "but can't do it here"
* Complete Bahasa Malaysia (Malay) translation of Puter
* Fix code styling
* Add Malay README.md
* Removed dash from `sub-projek`
* Make `Library` italic
* Add Malay language to `Translations` section
Adds a configuration parameter called "slow" to database config which
adds a hardcoded delay 70ms to database operations. This is useful
when testing for performance issues.
* Add context menu to apps in the start menu
* Start menu context menus: Close other menus
* More bugfixes for start menu context menus
* Start menu context menus: Add/remove from taskbar
* Bugfixes for adding to taskbar through start menu
* Highlight start menu icons when ctxmenu is opened
* Close context menus on start menu kbd navigation
* Reset start menu icon state when ctx menu closed
* Fix multiple start item context menus opening simultaneously
* Remove parent argument from start icon context menus
I translated the last part of the document for the italian language.
All the updates are typo-checked for errors , and there's none.
The translation has been made keeping in mind the italian use of various terms, some of the english words haven't been translated due to the extended use of the orignal term in everyday vocabulary.
* Refactor file system operations in GUI and puter.js to use eventual consistency for stat and readdir calls
This update modifies multiple instances of file system operations to include a consistency option set to 'eventual'. This change aims to improve performance and responsiveness by allowing for eventual consistency in file system interactions across various components, including helpers, UI elements, and IPC handling.
* Update cache expiration time for file system operations in readdir and stat to 1 hour
* feat: add network connectivity monitoring and cache purging
This update introduces a new feature that monitors network connectivity and purges the cache when the connection is lost. The implementation includes event listeners for online/offline changes and visibility changes to ensure cache consistency during network disruptions.
* clean up logs
* Implement the first naive version of `readdir` cache
* Purge the entire cache on every single mutation
Right now we're going to use the very naive, but safe, approach to purge the entire cache whenever there is change in the user's fs. We're going to incrementally improve this; but for now, better safe than sorry!
* Add socket event listeners to flush cache on file system item changes
This update introduces event listeners for 'item.added', 'item.renamed', and 'item.moved' events, triggering a cache flush on each event to ensure data consistency in the file system module.
* increase exp time for the cache
* Update readdir.js
* Update index.js
* refactor: update available_templates function for non-blocking template loading
- Changed the available_templates function to initiate loading templates asynchronously without blocking the UI.
- Initialized window.file_templates with an empty array and updated it once the templates are loaded.
- Added a console log in UIDesktop to track transaction duration upon completion.
* Update UIDesktop.js
Fixed spelling and missing accentuations
Adjusted capitalization for consistency with other strings
Made wording more natural in PT-BR (e.g. “site” instead of “website”, “Login” instead of “Início de Sessão”)
This commit removes debug logs that meet these criteria:
- used console.log instead of the log service
- were left in the source tree accidentally
- show up at boot or during stat/readdir operations
This is not a comprehensrve removal; this is a first pass to get a
few of the trivial cases.
- Introduced `input_image` and `input_image_mime_type` as optional parameters in the AIInterfaceService schema to enhance image processing capabilities.
- Updated `GeminiImageGenerationService` to differentiate between data URLs and web URLs for image handling.
- Added coercion support in CoercionService for converting data URLs to streams, including extraction of content type.
- Documented the txt2img method in AI.js to clarify usage for text and image-to-image generation.
Usually Puter has a UID by this point, but if it ever doesn't we can
fall back on using the path with a `LIKE ? + '%'` query. However,
users before a particular date might have fsentries without a path,
so we need to perform a check on the user's timestamp to avoid issues.
is_empty wasn't working correctly while running concurrently with
fetchEntry because because the check in fetchIsEmpty was too
specific. This commit fixes this issue while also adding support for
a 'path' parameter to is_empty.
- Enhanced the `generate` method to accept base64 encoded input images and their MIME types.
- Implemented validation for input image and MIME type to ensure proper usage.
- Updated the content construction logic to handle both text-to-image and image-to-image generation scenarios.
Earlier I actually thought this wasn't running in prod because when I
run Puter locally the "refresh app cache" message doesn't appear.
I ensured that minlog was off but I forgot that around the same time
I also disabled the "tick" log level by default. If it weren't for
that I would've realized this an hour ago. That said, it's kind of
odd that this seems to be the source of the leak, because this isn't
new behavior.
Replaced inline styles with CSS classes for the captcha modal to improve maintainability and readability. Adjusted JavaScript to accommodate the new structure and ensure proper functionality. Enhanced error handling and loading state management during the captcha verification process.
There used to be an app cache here that would be invalidated but I
think this may have been removed because the invalidation logic
was causing issues. A 30 second cache seems reasonable though, so
let's do that for now.
* chore(i18n/emoji): improve clarity of confirmation and name validation emojis
* chore(i18n/emoji): improve clarity of confirmation and name validation emojis
Previously ES6 Module extensions would only work on versions of node
with --experimental-require-module enabled by default, and provided the
entry file had no top-level await. This commit uses "await import"
conditionally if the "type" in package.json is set to "module".
Sometimes the same fs entry is fetched multiple times concurrently
because of parallel methods with separation of concerns.
After this commit, only one parallel fetch will go through and the
others will wait for it. Subsequent fetches will not occur is the
data that was needed is already there.
The 'quick_check' increases the amount of time it takes to perform a
stat operation significantly proportional to the time it takes to do
a stat because an fsentry fetch occurs before a transient
memoization map is available for an operation.
* the Arabic (العربية) translation of Puter is Completed
* Update ar.js
* updating the Arabic (العربية) translation of Puter
---------
Co-authored-by: jelveh <[email protected]>
For instances where svc_fs.node was called and explicitly passed a
NodePathSelector (instead of being passed a legacy-style object
with a path attribute) this code was failing. This commit moves the
affected code snippet after input coercion and updates references
to use the coerced input.
style: formatting changes were applied to the code. Instead of using
nested conditional blocks we use labelled breaks
Not sure when this went away, but we were always getting the
permission though the file path instead of ID/UUID from what it
looks like, which was hiding this problem.
Add a simple 20 second cache with kv.js to the scan_ method of
PermissionService. This will not improve the performance for
the permission system, but it will invoke it less often.
This is a quick solution for javascript files that should not have Puter
extension code prepended to the top of them. We may want to do something
different in the future like skipping directories named "gui" or
following imports (we can only do that with ESM though) or actually
having a way to run ES modules in their own global context (which is
avaialble in node.js as vm.Module under an experimental flag, but there
is no way to use the default linker for imports in the module);
basically there's no good way to do this and it is very unfortunate.
Previous commit to do this contained accidental other changes that I
staged and forgot about. This commit really only contains the sync for
package-json. This was broken for a while - since when we did work on
AWS secrets manager.
* feat: Differentiate tablets by pointer type
Reclassifies tablets with a mouse/trackpad to use the desktop UI.
This is detected by checking for `(hover: hover)` media query capabilities. This ensures that touch-only tablets retain their original, touch-friendly mobile UI while fixing the bug for users with pointer devices.
* Make sure `matchMedia` exists before using it
---------
Co-authored-by: Nariman Jelveh <[email protected]>
This error message in UIDesktop caused me confusion earlier because it
didn't provide any indication of where the error came from. This commit
adds a string behind the error to specify that it happened while trying
to launch an app.
This commit adds an undefined check for metadata from Puter's desktop
GUI inside a filesystem operation frame; this information won't always
be available.
Allows disabling abuse checks undefined origin checks with config
parameters to make development more convenient. The approach before this
change has been to manually comment out these lines of code.
The extension pseudo-global was only available at the root synchronous
execution context of a module loaded as an extension, which meant it
wasn't available for such things as handler functions for listeners even
though it looks like it should be lexically scoped.
This commit writes a `const` to the top of javascript files in the
runtime copy of an extension so that it works as nearly to a real global
as possible.
A real global isn't possible here for two reasons. First, running in the
same v8 VM as Puter means we can't have distinct global objects with the
same name across different extensions. Second, we can't use node:vm
because we would then be unable to support ES modules, which are
experimental in node:vm and also there's no equivalent to createRequire
for the linker even if we wanted to require the
--experimental-vm-modules flag in our package.json. This seems to be the
best we can do given the state of affairs of the runtime.
Some monitoring services want a system to look like it's failing hard
instead of a system being able to report that it's failing successfully.
This commit adds an alternative to healthcheck that throws 500 when any
service is failing, as though reporting such depended on the services
working, to appease these monitoring services and their semantically
incorrect nature of operation.
This commit message sets the precedent for "mon:" as a conventional
commit style prefix for changes related to system health and monitoring.
This optimization speends up load time for Puter significantly when many
extensions are present. This is also necessary for subsequent changes
which may require prepending code to all the source files inside an
extension, which would be too slow without this optimization.
We can assume Arch users are comfortable with making configuration
changes and observing high volume log output (maybe not true for
alternative distributions of arch, but for base Arch absolutely) so it
probably makes sense to have the log minimizer off for these cases.
* fix(gui): prevent background apps from stealing focus
* remove the `background` option as it's not really needed or used in other parts
---------
Co-authored-by: Nariman Jelveh <[email protected]>
While previously the /rename endpoint sent only a websocket message to
the client, move didn't fire a rename event if the name of the file had
changed. This commit adds a more generalized rename event that can be
reliably used to update a UI whenever the name of a file changes.
* Fix buggy arrow key navigation in start menu
* More fixes for start menu keyboard navigation
* Fix issue with start menu searchbar focus
* Rewritten start menu keyboard navigation
When you need to transform strings or other primitives inside an object
and preserve the object's structure in an optimally memory-efficient
way, this does that.
When /sign is called with UIDs instead of paths, it is possible to get a
response without 'path' properties present in "signed fs item" objects.
This commit addresses the issue by fetching paths from FS node objects
as a last resort.
- Implement a new method to serve a custom 404.html file if it exists in the subdomain root path.
- Update existing error responses to utilize the new custom 404 handling.
- Ensure fallback to default error handling if the custom file is not found or an error occurs during reading.
- Add translations for all previously missing keys in nn.js
- Add translations for UI elements including window titles, alerts, and desktop UI
- Maintain consistent formatting with proper indentation
- Add contextual English comments to aid future translators
- Ensure all keys follow correct naming conventions
Fixes#366
This commit expands on the previously implemented '%appdata%' value for
the 'path' option for "Open File" dialogs and allows it to be a prefix
for arbitrary subdirectories under an app's AppData directory.
This commit adds a 'path' option to showOpenFileDialog by supporting it
in the relevant conditional block in IPC.js. A special path "%appdata%",
which would otherwise produce an error in the file explorer (so no
conflict is possible), will resolve to the appdata directory of the
current app.
* Windows WebDav support
* feat: 2fa support for dav
* fix auth cookie when Basic auth present
* dev: dav: refactor handler, support root level navigation
* dev: dav: JSDOCing important things
* fix: possible edge case of password containing colon character
* feat: direct token login for webdav
* set cookie to not be session cookie
* fx: webdav undefined origin support
Allow apps to launch new instances or other apps with specific files.
The files must be passed as file signature objects, ensuring it was a
file the app was granted access to through a user action.
Adds a 'copy' mode to showSaveFilePicker. In this mode, a source file is
copied to the target file. The file may not exist in another apps
AppData directory. Unlike the 'move' mode, the file can be outside of
the app's AppData directory (a "user" file); in this situation the user
will be prompted to grant permission for the copy action. The app will
have access to the newly-created copy.
Adds service for querying controlling user for a domain. If no user is
verified as allowed to control the domain then we give control to the
'admin' user (to support local configurations of Puter).
After this point, I find myself a bit stuck because DNS queries are
incredibly difficult to test locally when you use tailscale.
With this commit alone, any user can specify "domain" on their
subdomains. This means they could affect the route for domains they
don't control, so this is not production-ready without further changes.
Temporary users can go through email confirmation which effectively
makes these users registered/permanent users. When email is confirmed
they should be moved to the default group for registered users.
* no more global puter object in workers
* also support my as a global in workers
* support incoming user object in workers
* also support myself in puter workers
In EntityStoreService, update and upsert both make use of the
detect_identifier utility function to get an identifier to pass through
to (eventually) SQLES.
For `update`, the `id` object is passed as the argument. Properties are
removed from this object when "redundant identifiers" are extracted to
create the update predicate.
For `upsert`, the `entity` object is passed and `id` is ignored. It is
suspected that this was done for reasons of improved developer
experience for developers using the API. The mutation behavior of
detect_identifier caused issues when trying to modify a property that
can also be used as an identifier, without having another identifier
available. (i.e. need to get UID of a subdomain to change the
`subdomain` property instead of just using `subdomain` to identify it).
This commit modifies the default behavior of detect_identifier to not
remove properties, then adds a flag to preserve the previous behavior
which is set TRUE by `update` since it's widelely used by puter.js and
has higher impact for potential regressions.
This doesn't completely fix the misalignment of selection area but improves a lot of issues with it. A temporary, middle-ground solution until I implement the full-on selection area rendering
* feat(gui): Enhance taskbar functionality and positioning
- Implement dynamic taskbar position management with options for left, bottom, and right placements.
- Update desktop dimensions and window positioning based on taskbar position.
- Add context menu options for changing taskbar position on desktop devices.
- Ensure mobile devices always display the taskbar at the bottom.
- Introduce CSS styles for taskbar positioning and adjust desktop padding accordingly.
* feat(gui): Reinitialize tooltips based on taskbar position
- Implement dynamic tooltip positioning for taskbar items based on the current taskbar location (left, right, bottom).
- Ensure tooltips are destroyed and recreated when the taskbar position is updated, enhancing user experience and consistency.
* feat(gui): Improve taskbar and tooltip positioning
- Add support for dynamic tooltip positioning for the 'top' taskbar location.
- Adjust CSS styles for arrow tooltips to enhance visual alignment and positioning.
- Refine taskbar item positioning for left and right placements to ensure consistent appearance.
* Improve popover positioning based on taskbar location
- Improve `UIPopover` positioning logic to account for left and right taskbar placements.
- Adjust Y position of popovers to ensure they appear correctly below toolbars for left/right taskbars.
- Update CSS styles for arrow tooltips to enforce consistent positioning with !important declarations.
* Refactor context menu positioning logic in UITaskbarItem
- Introduce a helper function to dynamically calculate context menu position based on the taskbar's location (top, bottom, left, right).
- Ensure context menus are positioned correctly relative to the taskbar item, improving usability across different taskbar placements.
* Fix UITaskbar window height calculation for consistent positioning
- Adjust the height calculation for windows in the UITaskbar to remove an unnecessary offset, ensuring accurate window sizing relative to the toolbar height.
- This change improves the visual consistency of maximized windows across different taskbar placements.
* Enhance window snapping and positioning based on taskbar location
- Introduce a new function to calculate snap dimensions and positions dynamically based on the taskbar's location (left, right, bottom).
- Update window snapping logic to ensure windows are positioned correctly relative to the taskbar, improving usability and visual consistency.
- Adjust boundary checks for window placement to account for taskbar height and position, enhancing the overall user experience.
* Update default taskbar position to 'left' and adjust CSS padding for desktop layout
- Change the default taskbar position from 'bottom' to 'left' in UITaskbar.
- Modify CSS to increase left padding for desktop taskbar positioning, enhancing layout consistency.
* Improve CSS for desktop layout with height adjustments
- Update CSS styles for input fields and desktop taskbar to set height to 100vh, ensuring consistent full-page layout across different screen sizes.
- This change improves the visual consistency and usability of the interface.
* Improve desktop selectable interactivity and taskbar tooltip behavior
- Add functionality to mark the desktop as selectable active, improving user interaction with desktop elements.
- Update tooltip display logic to only show when the desktop is not in a selectable state, enhancing usability.
- Adjust CSS styles for desktop layout, ensuring consistent behavior and appearance when the desktop is active or inactive.
* Update default taskbar position logic for first-time and existing users
- Set the taskbar position to 'left' for first-time visitors and default to 'bottom' for returning users without a saved preference.
- This change improves the user experience by providing a more intuitive initial layout for new users while maintaining consistency for existing users.
- Add new animation functionality to the taskbar; icons will enlarge when the mouse hovers over them
- Introduce new CSS properties and styles to support the animation effects
- Implement mouse move and leave event handling in UITaskbar
- Add UITaskBarCreateCurve and UITask components
* Update README.md
* Fixed issue: #1033 (Added app category display in Dev Center in main app list)
* Fixed issue: #1033 (Added app category display in Dev Center in main app list)
* test 1033
* Fixed issue: #1033 (Added app category display in Dev Center in main app list)
* Fixed issue: #1033 (Added app category display in Dev Center in main app list)
* Center the app checkbox
---------
Co-authored-by: jelveh <[email protected]>
- Added login and logout functionality with corresponding UI elements.
- Enhanced layout for user information display and login button.
- Integrated authentication state checks to update UI based on user sign-in status.
- Improved button styles and interactions for better user experience.
- Added user-select: none to disable text selection for better UX.
- Adjusted margins for buttons and elements to improve spacing and alignment.
- Modified the layout of the navigation bar for a cleaner appearance.
- Added a settings button to open a modal for configuring Puter.js URL and API Origin.
- Implemented functions to load and save settings to localStorage.
- Dynamically load the Puter.js script based on user-defined settings.
- Styled the modal and its components for better user experience.
This commit implements uploading Puter files through ai.chat using
Claude's beta "file inputs" feature. To use this, simply specify
`puter_path` in a content part, and that content part will be
transformed into the correct type of content part for the specified file
and the file will be uploaded.
- Introduced a "Reset results" button to clear test container backgrounds and remove error messages.
- Implemented hover styles for the reset button to enhance user interaction.
- Ensured the reset functionality hides the progress panel and resets progress tracking.
- Introduced two new tests: testGetPerformance and testSetPerformance.
- Each test measures the execution time of the respective method and asserts that it completes in under 100ms.
- Ensured correct value retrieval and performance metrics are logged for both tests.
- Added a progress panel to display real-time test execution status, including current test, progress percentage, and elapsed time.
- Introduced styles for the progress panel and its components to enhance visibility and user experience.
- Implemented JavaScript functions to manage progress tracking, including starting and stopping timers, and updating the panel with test results.
- Ensured the progress panel is responsive to test selections and updates dynamically during test execution.
- Adjusted the styles for the test counter to enhance its appearance.
- Removed inline styles from the test counter span for cleaner HTML structure.
- Ensured consistent font size and color for better readability.
- Added a fixed navigation bar for improved accessibility and layout.
- Introduced a test counter to display the number of selected tests.
- Updated styles for better alignment and spacing in the test UI.
- Ensured the counter updates dynamically based on checkbox selections.
- Updated the test UI to include a master checkbox that controls the selection of individual test checkboxes.
- Implemented logic to update the master checkbox state based on the selection of individual checkboxes and group checkboxes.
- Adjusted the margin for h2 checkboxes for improved layout.
- Added sticky positioning for h2 headers in the test section.
- Updated h2 elements to include checkboxes for grouping tests (File System, Key Value Store, AI).
- Implemented event listeners to synchronize individual test checkboxes with their respective group checkboxes.
- Simplified select/unselect all functionality to update group checkboxes accordingly.
All users have access to KV, however the permission system is used
because:
1. KV is a driver, and all drivers have access checks
2. The rate limit policy comes from the permission system
This change uses support for implicit permission shortcuts to prevent
any of the permission association tables from being read. It also
hard-codes the policy so that KV's rate-limit policy is not read from
the policy.json file.
Permission implicators with the "shortcut" option will be checked prior
to concurrent permission scanning. If any of these permissions are
granted, then the concurrent phase of permission scanning will not be
invoked; instead, the user will only see the shortcut permission.
It may be useful to add an option in the future for a full permission
scan, which could be useful in such things as a user interface to help
with permission management.
* experimental Cloudflare Workers for Platforms support
* add support for the puter driver
* stop hardcoding api.puter.localhost
* support destroy from express route as well
* initial worker support
* xhrshim + fixes (incomplete)
* change order of readyState + load event
* remove some debug logs
* change worker/puterUtils into a cjs module
* Cloudflare workers eventtarget workaround
* worker preamble webpack
* edit worker readme to reflect reality
* allow a way to code in api endpoint instead of hardcoding it to api.puter.com
* move cloudflare eventtarget fix to puter-portable template
This is so it gets run before the rest of puter-js initializes
* remove express route for worker
Backend had tests written in mocha as well as a test written for jest
(created by an AI utility, wasn't compatible with mocha tests). The AI
generated test had merit so it was migrated to vitest along with all the
mocha tests, which supports conventions from both frameworks.
Vitest also has an excellent watcher cli.
The root repo package.json now runs unit tests in backend. Before it was
only running tests defined in the _test method of backend services.
* fix: return proper error message when the model is not found
* use global source for the domain
Co-authored-by: Eric Dubé <[email protected]>
---------
Co-authored-by: Eric Dubé <[email protected]>
Closes#1326
- fix mis-handling of aliased models and DRY so it doesn't happen again
- fix where we try to get model details for a pseudo model
(usage-limited)
- fix metering with non-streaming mistral requests
Using LLOWrite outside of HLWrite, which is useful inside a Puter
extension when a simple write operation is being invoked by the backend,
required passing a thumbnail. However, sometimes this is not necessary.
This commit makes it possible run an LLOWrite operation without passing
a thumbnail stream.
The following changes make it easier to host web apps from inside a
Puter extension:
- expose express module to prevent mixing versoins
- allow registration of URLs exempt from origin requirement
Stack traces aren't logged when a health check failure occurs. This was
caused by interfacing with AlarmService directly instead of using the
error service.
The large if...else block for single-item vs multi-item selection
context menu items makes it difficult to extract common behaviors
between the two. For example, the "create shortcut" menu item could be
added by the same code snippet in both cases, and that snippet might
have conditional logic for the number of items (with 1 as a special
case).
The reason to move branching logic to the code snippets that handle
individual menu items is because the previous change to fix shortcuts in
shared directories revealed cross-cutting concerns in branching logic.
Since both single-item and multi-item "create shortcut" options both
have branching to handle the shared directory case, they are now more
similar than different.
Fallbacks from or to the fake-chat service should not be allowed.
Fallbacks to the "costly" model from fake-chat could result in
unintended charges and fallbacks to real models from fake-chat could
result in service abuse.
The SQL statement for creating a group uses syntax that only works under
sqlite. This was caused by introducing anomaly detection when a user
creates a large number of groups in quick succession.
This adds an experimental custom token authentication endpoint to Puter.
This is disabled unless the firebase-auth service is explicitly
configured, and is not yet ready for production use.
* Removed css property overflow: hidden at the parent container which was interfering with the layout calculations and was forcefully clipping child elements
* Replace css property overflow: hidden with contain: paint to fix stacking without masking border-radius
Fixes#1233
Already existing was the functionality to go to puter.local/@username to
access the Public folder belonging to "username". This commit adds the
ability for relative paths such as puter.local/@username/document.md to
access /username/Public/document.md in the suggested app for handling
".md" files.
This is the product of a couple hours of debugging. We can now remove
the uuid entry from the cache for a deleted user without the strange
lockup behavior that was being observed previously. However, it is still
explained exactly how this happened; while this commit addresses the
cause it does not represent an actual understanding of the issue.
What is known is the following:
- /delete-own-user can trigger a complete lockup
- this happens when invalidate_cached_user is called
- kv.del('users:uuid:<uuid of user>') triggers the issue
- ... because get_user returns null and
- configurable_auth middleware accepts the null value
- configurable_auth middleware DOES call next()
- it is unknown why a lockup occurs after this
This allows a logger to be specified in the execution context. If
LogService sees this, it will perform its usual logging but also call
the injected logger.
Driver interfaces don't require specifying the structure of the return
value when it's a JSON object, but parameters require specifying what
parameters can be provided. This commit adds a special wildcard
parameter for interface definitions. When the wildcard parameter is set,
the entire input object is considered as if it were the value of one
parameter.
Instead of puter.drivers.call, make it puter.call. This is accomplished
by allowing puter.js modules to mutate the `puter` object on
initialization. Support for an optional `_init` method, similar to
backend services, is added to Puter modules to help with this.
This commit modifies puter.js to allow calling an
interface+service+method with matching names without redundantly
specifying. The 'ip-geo' service is also renamed to ipgeo to match its
method name and allow a simple:
puter.drivers.call('ipgeo', { ip: '1.2.3.4' })
This commit adds the concept of a toLogFields method on objects which
will determine how they're turned into log fields in LogService. This
method is now implemented on actor to prevent errors when the object is
passed to log functions without being stringified first.
I wasn't able to reprod any memory leak from a user having a large
number of files locally, but it's showing up in logs and maybe could add
up with lots of temp users getting auto-removed. It's worth a shot.
There were issues with async handling in events which can make it
difficult to use event handlers to mutate values provided by an emitter.
This is not an optimal solution: this will await event listeners in
sequence. Ideally, event listeners should be awaited concurrently.
Usage/cost tracking per user was over-fitted to LLM/AI services. This
commit adds CostService and updates AIChatService to use that instead of
updating the `ai_usage` table. The `ai_usage` table is now considered
deprecated.
A service called PermissiveCreditService is enabled by default in
SelfHostedModule. This is so users who host their own Puter instances
don't have to worry about usage limits, since they're providing their
own keys anyway.
Previous logic was:
- on email change, update temporary value
- send email to confirm new email
- temporary value is moved to real email
This doesn't work when an account has not yet confirmed their email
after signup (i.e. user's email_confirmed is still 0). Well, it actually
does work, but the user is only able to confirm the change to their
email and not set their account as having a confirmed email.
New logic has a branch for this case; IFF email_confirmed=0:
- change account confirm code
- send new account confirm email
A logo addition from another PR snuck in. This definitely makes it
consistent with signup, but this logo broken! This commit makes it be
not broken anymore.
* refactor: migrate interfaces.js to new registration mechanism
- Created EntityStoreInterfaceService for crud-q interface\n- Created AnalyticsInterfaceService for puter-analytics interface\n- Added InterfacesModule to load these services\n- Removed interfaces.js\n\nCloses #1131
ai: true
* chore: remove interfaces.js file
ai: true
* fix: DRY CRUD interfaces
This comment is flagged as AI-generated, but Claude rate-limited before
it could actually make the commit so this commit was made by hand. Well,
while I'm writing this commit message I may as well mention that Claud's
rate limits are relentless and it has become impossible to use Claude
for some purposes as a result.
ai: true
* refactor: replace interfaces module with separate Module.js files for entitystore and analytics
- Removed interfaces module\n- Added EntityStoreModule.js to entitystore module\n- Added AnalyticsModule.js to analytics module\n- Updated main index.js to use the new modules directly\n\nai: true
ai: true
* fix: modules exported and registered incorrectly
* feat: add KVStoreModule for puter-kvstore interface
- Created KVStoreModule.js\n- Created KVStoreInterfaceService.js to register the puter-kvstore interface\n- Updated exports.js to include the new module\n\nai: true
* fix: remove index.js from kvstore module
- Removed unnecessary index.js file from kvstore module\n\nai: true
* fix: remove index.js files from analytics and entitystore modules
- Removed unnecessary index.js files from analytics and entitystore modules\n\nai: true
* fix: cleanup mycoder mistakes again
...because it actually threw out my previous commit where I already did
this.
* Added Revis distributed cash to enhance our Captcha Verification system so that we prevent our system from replay attacks
* Fix: There was an error with the implementation of Redis, so I reverted to our previous version that uses in memory storage
* Integrated the captcha verification system into our sign in Form. The captcha verification system now works on both login and sign int
* Remove test files from captcha module
* Update src/backend/src/modules/captcha/middleware/captcha-middleware.js
Co-authored-by: Eric Dubé <[email protected]>
* Update src/backend/src/modules/captcha/middleware/captcha-middleware.js
Co-authored-by: Eric Dubé <[email protected]>
* Now the captcha can be requested on condition, this llaows extenstions to control wether a captcha should be required,
I fixed the code in CaptchaModule to use config
and got rid of the lines that made captcha middleware available since it wasn't used anywhre
* I split the middleware into two distinct parts, so that the frontend can now determine captach requirements. PuterHomePageService can set GUI parameters for captcha requirements. The /whoarewe endpoint provides captcha requirement information and the extensuo system integration is maintained
* Fix security issues with password handling in URL query parameters
* Made sure that the enter key, submits the login request instead of refreshing the captcha
* In development we can now disable the Captcha verification system by running it with CAPTCHA_ENABLED=false npm start
* Went back and modified checkCaptcha so that it checks at the start to check what CAPTCHA_ENABLED is equal to
* Refactor captcha system to use configuration values instead of environment variables
* Fix captcha verification and align with project standards
* Update src/backend/src/modules/captcha/README.md
Co-authored-by: Eric Dubé <[email protected]>
* fix: incorrect service name
* dev: use Endpoint for captcha endpoints
Use Endpoint class, which uses eggspress behind the scenes, which handles
async errors in handlers automatically.
* dev: add extension support and simplify captcha
- removed extra error handling
- removed dormant code
- no distinction between login and signup (for now)
* clean: remove local files
* fix: undefined edge case
---------
Co-authored-by: Eric Dubé <[email protected]>
* feat: add public endpoint for models list
- Created ChatAPIService for public endpoints\n- Added /chat/models and /chat/models/details endpoints\n- Registered service in CoreModule\n- Added tests for the new service\n\nCloses #1227
ai: true
* Update src/backend/src/services/ChatAPIService.js
* Enhanced ai command to perfom other commands
* Enhance AI Command in Puter's shell
* Enahanced ai command to use tooling/function calling
* Fixed circular dependency and added list function to Builtincommand
* Fixed circular dependency and system prompt
- Clarified that the Nodemailer example is for testing MailHog setup\n- Added information about how Puter uses Nodemailer\n- Added references to the EmailService class
ai: true
* docs: improve PuterAI module documentation structure
- Update docmeta.md to describe top-level doc structure\n- Create README.md for PuterAI module documentation\n- Move requests.md content to api_examples.md\n- Add ai_usage_testing.md in contributors directory\n\nCloses #1215\n\nai: true
* docs: remove original requests.md after content migration
Content has been migrated to api_examples.md with improved organization and structure.\n\nai: true
- Renamed 'options' to 'requestParams' to clearly indicate parameters sent to the backend driver\n- Renamed 'settings' to 'userParams' to clearly indicate parameters provided by the user\n- Updated comments to be more descriptive about the purpose of each variable\n\nFixes #1212\n\nai: true
Modified the event handler in AIChatService to only skip usage reporting for fake-chat when it's not using the costly model, allowing the costly model to properly report its usage and costs.
ai: true
Added _init method to FakeChatService to register itself as a provider with AIChatService, ensuring that the service and its models are properly recognized by the system.
ai: true
Added a new 'costly' model to FakeChatService that simulates incurring costs similar to real AI services. This model can be used for testing cost tracking without incurring actual external API costs.
ai: true
This change modifies the SQL query in check_usage_ to only consider AI usage from the past month when checking against limits, rather than all historical usage.
ai: true
* Add support for pasting multiple file extensions at once in Dev Center
* Fix bulk file extension paste functionality in Dev Center
* fix: add try-catch around tippy
* Fix issues with bulk file extension pasting and add comma key support
* Fix issue with duplicate red tags appearing temporarily
* Implement robust bulk file extension paste functionality in Dev Center
* Fix security issue with JSON.stringify to properly escape < characters
* feat: hide icons/show icons feature added to the right-click/secondary click menu on the desktop
* Fix: Changes made to the hide/show desktop icons feature after receiving feedback in the first pull request
* CSS rule added to static CSS file instead of being implemented dynamically
PR 1175 introduced a bug that wasn't caught where alert messages are not
properly assigned. Additionally, the default alert type was "warning"
which is a regression. This commit makes "info" the default alert type
and ensures options.message is the first candidate for the alert message
contents.
* Update README.pl.md
Things I would change:
Line 1: System Operacyjny Internetu -> System Operacyjny Internet
Line 25: system operacyjny internetu -> internetowy system operacyjny
(Original Translation is more like Internet's OS, not Internet OS)
Line 36: Rozwój Lokalny -> Instalacja Lokalna
(Original does not mean programming, but more like growth or evolution)
Line 97: Połącz się z opiekunami -> Skontaktuj się z opiekunami
(Original has meaning, like connecting by cable, not contacting)
* Update README.pl.md
Changed:
Instalacja Lokalna -> Lokalne Środowisko Programistyczne.
This is a step towards getting puter.js to run in node instead of just
in the browser. In node, all the relative imports need to end with the
file extension, and the "type" option in package.json needs to match the
import mechanism used. It was also necessary to migrate
webpack.config.js to ESM syntax, which I would not have done if the
tooling didn't force me to.
* documentation: new documentation for emit features
* Update events documentation and doc_helper.js script
* Update package.json
Co-authored-by: Eric Dubé <[email protected]>
* added manual_overrides.json.js file
* Update doc_helper.js to resolve paths relative to project root and update events documentation
* created a new events.md generated file, differences can be seen.
* added and populated events.md file. Can now compare new and old versions
---------
Co-authored-by: Eric Dubé <[email protected]>
ESLint cannot support our preferred code style for backend without
writing custom plugins. On frontend, we already decided ESLint wasn't a
good fit several months ago. This commit removes the eslint config to
avoid other developer's editors making unwanted whitespace changes.
In backend, we use spaces within control structure parens. This really
does make the code easier to read. In the future, maybe we'll write our
own linter.
* add API to get a list of all available models
* modified listModels() and listModelProviders() for readability.
* remove dead code
---------
Co-authored-by: avijh <[email protected]>
* feat: create and export UsageLimitedChatService for when user exceeds usage limit
* tweak: change comment on usage-limited-chat to better explain action
* fixed whitespace
* tweak: remove test-app from gitignore
* tweak: remove extra spacing in AIChatService usage-limited-chat comment
* tweak: fixed whitespace
* tweak: remove tabbed whitespace in AIChatService
* tweak: remove .qodo from gitignore
* tweak: remove extra enter in AIChatService
* Revert "tweak: remove .qodo from gitignore"
tweak: correct mistake on removing extra enter
This reverts commit e066e294fa.
* tweak: add space at fallback and remove whitespace tabs
* tweak: remove whitespace on brackets
* tweak: remove .qodo and test-app from gitignore
* tweak: add accidentally deleted enter back in
---------
Co-authored-by: Rishabh Shinde <[email protected]>
* Added requestPermission endpoint in SDK, updated IPC handler HeyPuter/puter#1150
* - Updated UIWindowRequestPermission.js to accept multiple permission types
- Updated dialog message for permission window in UIWindowRequestPermission.js
- Updated parameters for call to UIWindowRequestPermission in IPC.js
- Added search_uid.js endpoint to allow GUI searches for fsentry by file UUID HeyPuter#1150
* Updated body and header for Permission Request Dialog HeyPuter#1150
- Replace app uid with app name for header in UIWindowRequestPermission.js
- Added path for file permission request body in UIWindowRequestPermission.js
- Removed previously added search_uid.js api implementation previously as it is replaced by simpler call in gui
* Updated permission description generation in UIWindowRequestPermission.js HeyPuter#1150
* Removed incorrect web handling in IPC.js HeyPuter#1150
* Formatting fixes
* UIAlert with dynamic icon types and default button configurations
* Update src/gui/src/UI/UIAlert.js
Co-authored-by: Eric Dubé <[email protected]>
* Update src/gui/src/UI/UIAlert.js
Co-authored-by: Eric Dubé <[email protected]>
* fix: improve error handling in UIAlert component and reseting package-lock
* fix: update UIAlert to use 'type' for alert type selection
* fix: update UIAlert to use 'type' for button configuration
---------
Co-authored-by: Eric Dubé <[email protected]>
Flex isn't being used in the top bar correctly, which is causing
unpredictable issues when people implement new elements on the top bar.
The best solution for this is to use proper flex values but right now
it's important to get the layout back to normal for production so I'm
implementing this sub-par fix where we always include a spacer element.
* Update README.md
Without clear instructions it wasted a few hours just for setting up.
Atleast no one will face issues with domain name in localhost now
* Making LocalHost website visible
* Making LocalHost website visible v2
I really screwed that up on my first attempt. Puter apps don't trigger
the user-connected websocket event, which rendered thread subscriptions
in apps useless. Furthermore, the uuid of the thread that was subscribed
too wasn't present in the event, which is also pretty useless.
As far as I remember, PermissionService was not using APIError because
it was considered to be only for errors from API endpoints and not
internal calls. However, this never made very much sense: APIError is
really more like "error that Puter's backend actually knows how to
handle" at this point; everything should be APIError and APIError should
probably eventually be renamed.
We were sending a JSON error in what should have been an HTML response.
Additionally, the error message wasn't very clear about the nature of
the problem.
This commit makes everything a ton easier for deployers, adding
configuration options to allow nip.io domains or all origins to access
Puter for ideal LAN or VPN configurations.
I used 'fix' for this instead of 'doc' because I am also adding a
package for code that generates markdown, so this doesn't have the
zero-risk nature typically associated with a "doc: " commit.
I uploaded a video for this one but just realized I never made a commit
for it - it was hiding in my git stash.
So here will be a rare situation that the video link is able to be
placed inside the commit message: https://youtu.be/8XiFR1o5Vuk
As far as I can tell from the block post:
https://blakeembrey.com/posts/2024-09-web-redos/
this vulnerability should not affect releases of Puter before this
update because we do not have any routes with multiple parameters where
the second parameter does not start with '.' or '/'.
However, for the sake of good security hygiene and so `npm audit` looks
nice, we're upgrading the package. (better late than never)
Change username doesn't go through ll_move, and does not adhere to DRY
alongside the /rename operation. This led to an inconsistency where the
rename behavior in the change username operation didn't update the
cached fsentry path.
Some services can't tolerate a message with tool calls and an empty
array set on the content property, despite the fact that OpenAI does
seem to support this.
Mistral has a lot of unnecessary quirks because the SDK coerces to
snake_case. We should see if the OpenAI client works with Mistral, and
it it does we'll use that instead so we don't have to maintain all these
exceptional cases.
This adds an incremental migration from models reporting input and
output tokens, to models reporting full token information. This means
AIChatService doesn't need to lookup the model details or know about the
different types of tokens supported by every model.
The current implementation would break for XAIService because it gets
cumulative usage reports as the stream goes on (whereas OpenAI and
DeepSeek only provide this at the end of the stream).
If these APIs were standardized this would have been so much less
painful, but alas, tool calls are implemented completely differently
between Claude and OpenAI. Good job guys.
- add merging of same-role messages for all normalized inputs
- generalize Claude-specific system message extraction
- remove redundant behavior from Claude
TimeWindow is used by TrackSpendingService for two purposes:
- tracking cost within a time window
- preventing duplicate alarms within a cooldown period
It is important to test this to ensure alarms related to AI costs are
accurate.
Sometimes tool use requests are streamed as JSON, but sometimes they're
streamed as an empty string (causing the JSON parser to error). Had they
not streamed anything we actually would've responded correctly, but
what they stream implies "this empty string is the JSON input" which is
incorrect. Oh well.
The call to sock.write() wasn't waiting for the 'open' event, which
worked fine over non-TLS sockets by chance. Also, responses with only
a single WISP packet were resulting in buffered response data not
being sent to the caller.
One of the test cases has been rendered redundant so it has been
removed. Another test was changed to overwrite a file as it's expected
to already exist.
There was an issue affecting mkdir requests with create_missing_parents
set to true which prevented the correct behavior if some of the listed
parents were already present. This commit ensures mkdir selects the
deepest existing node as the parent before proceeding.
These used to serve a purpose but now they only cause a dependency loop
between LL*/HL* operations and FilesystemService itself, making it
difficult to strategize filesystem behavior.
When getting a stream for an SVG icon from AppIconService with a
previously-fetched stored icon provided, a double-encoding bug was
preventing a valid image from being obtained.
Adds an event for app invalidation.
Moves iconify_apps to AppIconService and adds RecommenededAppsService,
which holds some logic that used to be in get-launch-apps. The event for
app invalidation is then used by RecommenededAppsService to determine
when to clear the recommended apps cache.
Moves AppIconService, OldAppNameService, and ProtectedAppService to the
new apps module to keep things organized, and updates imports. There
were no interesting init errors. Services creating a database at init
were tested for failures but this wasn't an issue. It turns out
initializing with a database accessor before the db service inits is
okay, but using that database accessor at init is not.
There are now several services related to apps, and
RecommendedAppsService is about to be added, making this a good time to
create a separate module for these services and migrate the legacy
service AppInformationService.
During migration it was realized that most of the initialization needed
to occur at boot.consolidation rather than boot.init because the
database needs to be initialized first.
There were a few things which made this rather difficult:
- AWS SDK doesn't appear to have a utility to validate message
signatures.
- The only available node.js module that does this is known to be
unreliable.
- SNS sends 'text/plain' MIME type even though the data is in JSON
format, so a middleware had to be added to account for this.
- We don't accept POST requests with no Origin header. Since SNS doesn't
send the Origin header, an exception had to be made for this.
- The endpoint needs to be public and SNS doesn't seem to have a
proxying utility for developers such as what Stripe has.
- Because of the above point, debugging time was affected by deployment
time to the staging server.
This commit fixes the timing of the /whoami call used for the
PrefixLogger (a logger for puter.js that displays app names) to ensure
the API origin and token are available. This is done by waiting for a
service called 'api-access' to be initialized first.
This was discovered while testing changes for /whoami responses that
display icons, because the current behavior breaks the call for
non-puter.com instances.
Calls to `/whoami` generally don't require app icons. Fetching icons of
the smallest size still incurs round-trip latency from the database and
S3, as well as additional delay for any retries due to failed icon
generation.
Although this is technically a regression in `/whoami`, the impact is
localized to Puter's desktop only because apps are not allowed to
receive taskbar icons.
Current issue with get-launch-apps wasn't fixed by catching errors while
reading the stream so this next attempt catches errors from the call to
start reading.
If the error occurs asynchronously this may not work either.
This happened because the return signature of get_app_icon was changed
so that the MIME type can be returned when the uploaded or default icon
is being used as a fallback. AppES was not updated to account for this
change.
Fix explorer table was always too wide with extra white space on right side causing an unnecessary horizontal scrollbar. Changed width for both the header row and item rows from 100vw to 100%, and added min-width.
Apparently there were two things:
- had to remove contextlink from the test command (that was expected)
- can't import libs from putility in the test environment. The error
produced for this doesn't really explain why.
Updated the romanian translations.
I kept the terms "Professional" and "Business" in English as they are commonly used this way in Romanian in IT services context
I adapted the forms of address to second person singular ("tu") instead of formal ("dumneavoastră") to maintain a friendly and modern tone
I maintained consistency with existing translations in the file
Placeholders (%strong%, %%) were kept in grammatically correct positions for Romanian language
A legacy mkdir function was still being used by the
generate_system_fsentries function. However, it wasn't necessary because
we now associate fsentries by uuid, eliminating the requirement to
create the home directory separately from its subdirectories.
The old mkdir function was removed, and generate_system_fsentries was
moved to a new service called UserService, which is intended to
eventually hold all methods related creating, removing, and updating
users.
* added validation function
* tagify implementation (incomplete)
* tagify implementation (completed)
* feat:implemented Tagify and filetype validation on filetype associaction in dev center
* tweak: removed unnessary tagify from dependency (its streamed from CDN)
* removed unnecessary tagify import
Currently the AI comment writer is trusting the AI to write comment
markers. This means sometimes the syntax of a source file is broken by
comment generation. The instructions in the prompt were pretty clear, so
rather than --fixup'ing the previous commit I'm adding these changes
separately as an interesting case study of how different models behave.
Following this result, I am going to be removing mistral from the
comment writer. It is the most likely to misbehave.
I will also be removing gpt-4o-mini, which sometimes included source
lines despite the "no surrounding text" instruction.
Complete missing Igbo translations in src/gui/src/i18n/translations/ig.js
- Translated all missing keys in the Igbo localization file.
- Ensured cultural appropriateness and accurate representation of English text.
- Carefully handled placeholders like %strong% for proper placement within the translated text.
Adds AIChatService, an implementor of puter-chat-completion which can
delegate to other implementors (implementors that have registered with
AIChatService at initialization) based on details of the request.
Makes AIChatService the test implementation. AIChatService then
delegates to FakeChatService when in test mode.
Adds `models()` method to puter-chat-completion. This method, instead of
returning only the names of supported models, includes other details
such as the cost and maximum output size.
Implements `models()` on Claude and XAI.
Registers Claude and XAI with AIChatService.
Replaces Claude with Grok pretending to be Claude. This is in lieu of a
proper fallback system which will be implemented later.
Grok will always say it's pretending to be Claude rather than
identifying itself as Claude.
This new endpoint shows permissions granted by the current user to the
specified user in the scope of a specified filesystem node, including
permissions on the affecting parent nodes.
This is related to two future tasks:
- prevent users from sharing a file with the same user twice
- add an endpoint to modify the access mode of a shared file
Recursively checking for files with 'see' permission? What was I
thinking? This replaces the disasterously awful approach of scanning a
user's entire file tree to find shares, with an approach which instead
selects on the permissions table and extracts file UUIDs from
permissions.
I need this for development/debugging purposes. I figure I may as well
start developing the system dialog we'll eventually have to create
rather than make a temporary debugging element. This window could
eventually become a full-featured group manager, and can easily be moved
to a settings tab.
This was a really small refactor - about 30mins - that moves the concern
of common constructor args for modules outside of each individual call.
A Context object is now used for common constructor arguments. Some of
the values on this object - such as APIOrigin and authToken - are
following values on the instance of the Puter class. This means that for
some modules it is already possible to eliminate the setAuthToken and
setAPIOrigin listeners (out of scope for this commit). Any which remain
could eventually be replaced with a listener on the Context object
itself.
This commit also moves the initSubmodules method to the top of the class
so that it's easier for new devs to find, in case they're looking into
an issue on a specific module rather than the Puter class itself.
The kdmod module was broken since we renamed packages/ to src/. Also,
the update to mod loading for some unknown reason now requires each call
to require to end with `.js`. As far as I know, this was working
previously.
Allows modules to register a listener to the 'install' event without
creating a Module class. This changes how external modules are
installed.
External modules are now referred to as "extensions"; this commit does
not update the term but does use 'extension' as the name of the global.
This was hilariously difficult to debug. This isn't the first time an
issue like this occurred; this happens because of a deviation between
mysql and sqlite, where a JSON-typed column in mysql will return as a
native object in queries, whereas a JSON-typed column in sqlite is a
string column and will therefore return as an un-parsed JSON string in
queries.
Fixes errors that were noticed being thrown by launch_app, specifically
in the branch of logic when launch_app is called NOT via ExecService.
This branch of logic was added to fix an issue where notepad wasn't
prompting to save unsaved changes on close because data-appusessdk was
not properly set to true. The fix still worked, despite throwing these
errors, because setting data-appUsesSDK to true is the first thing it
does. However, broadcast messages to apps launched with launch_app NOT
via ExecService would have been broken.
Adds a filesystem relay service to GUI-side which currently reports an
exmaple fsentry for /stat and /readdir. Added filesystem services for
both sides. Moved filesystem implementations from src/modules to src/lib
since they're being called by services now rather than modules.
This functionality is effectively disabled unless
src/puter-js/src/services/Filesystem.js is updated to use init_app_fs_()
when the environment is 'app'.
Adds XDIncomingService to manage messages from another window. IPC now
registers with XDIncoming and reports whether it was handled, that way
messages intended to reach GUI can be reliably ignored.
On GUI-side, XDIncomingService will be used by FSRelayService (not yet
implemented) to handle requests for filesystem operations from another
window.
On App-side, XDIncomingService might be used by the PostMessageFS
client-side filesystem implementation terminal to listen for filesystem
events that were relayed by GUI (after a permission check) from their
original websocket source. Either that, or we'll open a filesystem
socket for each app using that app's token.
NoPuterYetService was added because IPC.js is loaded before
globalThis.puter exists, and was the easiest way to still allow IPC's
listener to be registered with XDIncomingService.
APIAccessService was added. This service currently holds auth_token and
api_token and does nothing else. FilesystemService listens to this to
maintain a websocket connection. APIAccessService will help to manage
the complexity of all further code dependent on being informed about
changes to the auth token or origin. Currently in puter.js these are
passed around to several modules which manage the same piece of state
information independantly.
mkdir operation was passing user instead if actor. I really think I
changed this already; getting a crazy deja-vu feeling about it. Maybe a
commit was lost somewhere along the road, but not sure.
Dispatching and listening to events is non-trivial. The apparent
triviality is in implementing a list of listeners and calling them. The
non-triviality is in the nature of what happens to a system when it has
multiple different interfaces to register listeners and publish events.
This commit adds TopicsFeature, which allows any class extending
AdvancedBase to declare topics. A topic is a simple pub/sub channel.
TopicsFeature will manage the state of listeners so the class doesn't
need to. A GC-friendly mechanism for detaching listeners is also provided.
The ServiceManager will replace manual management of services within
initgui, and will also be used within puter.js. Eventually Puter's
backend might use this instead of the existing Container class, although
this will be a large change that needs to be done incrementally.
The difference between ServiceManager and Container is the logic behind
when initialization occurs. Rather than have all services initialized at
once when Container's init() method is called, services are initialized
as soon as their dependencies have been initialized.
Adds CachedFilesystem layer to client filesystem chain. Currently only
stat is implemented. The stat implementation will hold onto an entry for
3 seconds as per hardcoded configuration. Eventually, once invalidation
via websockets is working, this TTL should be extended.
Support for trait method overrides will make it possible to use putility
traits with the decorator pattern while using a proxy class to avoid
redundant re-implementation of proxy methods.
Use of the pattern described above will occur in the implementation of
client-side filesystem caching.
- de-coupled xhr callback passing from the interface of the underlying
filesystem implementation.
- This makes the interface to delegate calls more suitable for use with
the decorator pattern.
- The decorator pattern will be used to manage the complexity of the
caching layer by separating the concerns of different caching methods.
This hides database entries with empty types from the list of suggested
apps for files that have no extension. These entries should also be
removed from the database and the bug causing them should be fixed in
dev center.
This could not be tested locally due to another but only affecting
self-hosted installations. This will be tested in staging in the
meantime while the other bug is being fixed.
Return to phoenix shell when a pty stream is closed by twisp.
Pipes in phoenix with commands from the emulator do not appear to be
working properly, but something in there is working.
This involves establishing the protocol through which phoenix instances
run a command on the emulator. The pty is able to communicate with the
terminal in both directions. This commit adds logs to be removed later.
There are a few things left that this commit does not address:
- handling close of delegate process
- handling sigint from phoenix to delegate process
- closing the connection to twisp
I'll elaborate here because the commit name is confusing. Any time an
app gets a connection to another app, either because it launched that
app (or was launched by that app) or requested a connection to that app,
the ID the app gets to represent the app it's communicating with is now
a pseudo app id rather than the app instance ID. This accomplishes two
things:
1. It's more secure. There are multiple places where GUI assumes that
knowing an app's instance ID means you can do things as that app.
2. Between the same two apps, there may now be more than one connection.
This is useful for situations like Phoenix shell talking to the
emulator in multiple separate instances to pipe commands. (this is
coming later)
Changing the email address on an account that did not already have a
confirmed email would result in the email_confirmed column never being
set. This commit fixes that.
Additionally, an SQL statement using the unconfirmed email to identify a
row was changed so it now uses the user ID instead, which is safer.
Resolves: 759
The literal translation for Pictures IS Gambar-gambar, but after reviewing how it looks on live demo Puter, this translation seems a bit off, since other word like Videos is not Video-video, documents is not Dokumen-dokumen.
So, I think it's better to just translate Pictures to Gambar only. Indonesian actually does not care about singular and plural. If we really need to emphasize it plural, we just repeat the word. Which in this context, it is not that necessary to emphasize it.
This is in preparation for the chat completions driver. OpenAI asks that
a user ID be provided in requests when service is being provided to
other users, so we deterministically generate different UUIDs for this
purpose to prevent user information from being exposed.
This issue was caused by an INSERT statement which adds the admin user
to the admin group. The admin user is created at initialization, so for
new installs it does not exist at the time migration scripts are run.
Additionally, new javascript-coded migrations (.mig.js) would not have
run for new installs because of redundant (WET; not DRY) code for
initial setup.
This commit also improves error handling of the migration service, which
was necessary to diagnose this issue.
In the future, setup always needs to be tested after updating migration
scripts, even though migration of existing databases is where most of
the issues will happen.
Resolves: #639
Guidance for AI coding agents working in this repository. Read this before making changes. FOLLOW GUIDANCE AS CLOSELY AS POSSIBLE. If you think the guidance is wrong, raise an issue or flag a maintainer — don't just do what you think is right. This is the source of truth for how we write code, tests, and docs in this repo.
## Documentation Index
Use these as the source of truth before exploring further:
- [README.md](README.md) — project overview and quickstart.
- [doc/contributing-apis.md](doc/contributing-apis.md) — adding and maintaining public APIs end to end (backend surface → puter.js → types → docs → tests). Follow it for any API work.
- [doc/pagination.md](doc/pagination.md) — the one pagination convention for list APIs (limit/cursor/offset/includeTotal, envelope shape, cursor semantics).
- [doc/alarms.md](doc/alarms.md) — raising alarms and picking a severity (what pages, what only gets recorded), plus the config that routes them.
- [SECURITY.md](SECURITY.md) — how to report vulnerabilities (do not file them publicly).
- [BUG-BOUNTY.md](BUG-BOUNTY.md) — bounty program scope.
- [TRADEMARK.md](TRADEMARK.md) — trademark usage.
---
## Repo-wide conventions
These apply everywhere — backend, puter.js, and GUI.
### Language & files
- Write ES modules, not CommonJS — we transpile and build as needed.
- TypeScript preferred for new files in the backend and extensions; convert existing JS there opportunistically when you're already touching a file. The GUI and puter.js are plain JavaScript — don't introduce TypeScript files in them. In puter.js, type with JSDoc instead (see the puter.js section for what must be typed).
- **Reuse before adding.** Search for an existing type, helper, or implementation and extend it; only add a new one when nothing suitable exists.
- Make new types findable: descriptive `PascalCase` names, exported from the obvious entry point. A type used from many places belongs in the owning module's `types.ts` (e.g. [src/backend/controllers/types.ts](src/backend/controllers/types.ts)) rather than bloating a logic file; a type with a single consumer can stay next to it.
- Naming: `camelCase` for variables/functions, `PascalCase` for classes and files containing a class (`AuthService.ts`). **Prefer `camelCase` for all new code** — new local variables, parameters, functions, and internal object properties. `snake_case` is reserved for identifiers that are part of an external contract: wire/JSON keys sent to or received from the backend, stable API error codes, public option names that already ship as `snake_case`, and established `puter_*` namespaced fields. Don't rename those (it's a breaking change), but don't introduce new `snake_case` alongside them either — a camelCase local can carry a value into a `snake_case` wire key (`{ operation_id: operationId }`).
### Comments
Keep comments light; prefer self-documenting code. Comment only when the _why_ is non-obvious or a usage detail would trip the next reader. Use `//` for single lines and `/** ... */` JSDoc when it genuinely needs more — if a comment runs long, it's probably too long. Don't restate the code and don't reference the current task, PR, or version — those rot. **No ticket references** (`PUT-1234`, `// fix for FOO-99`) in code, comments, or test names; describe the why in domain terms, not project-management terms. Use plain ASCII `-` in comment section dividers (`// -- Section --`), never box-drawing characters.
### Security & privacy
Before opening a PR, scan the diff for:
- Logs, error messages, or responses leaking internal paths, secrets, tokens, env vars, or other users' data.
- Debug routes, test credentials, commented-out auth checks.
- Endpoints returning more than the caller actually needs.
When in doubt, return less. Auth-, permission-, or data-export-related changes deserve an explicit callout in the PR description.
### Working rules of thumb
- **Run it, don't just compile it.** "It type-checks" is not "it works." Exercise the code path end-to-end at least once.
- **Read the neighbors before writing.** Match the shape of similar things already in the tree. If you think the existing pattern is wrong, raise it — don't quietly diverge.
- **Test new behavior.** Every new function, endpoint, driver method, or logic branch gets a test; every bug fix gets a regression test that fails before the fix. If something is genuinely hard to test, skip it but say so in the PR.
- **Boy Scout Rule, proportional to the change.** Fix the obvious typo or dead import in files you're already touching; don't ride a refactor along with a bug fix.
- **Understand what you commit.** AI assistance is fine; shipping code you couldn't defend in review is not.
---
## Backend
A layered stack with explicit dependency injection: each layer depends only on the layers beneath it, receives them through its constructor, and `PuterServer` ([src/backend/server.ts](src/backend/server.ts)) wires the whole thing together. [doc/architecture.md](doc/architecture.md) is the full reference.
| **Controllers** | [src/backend/controllers/](src/backend/controllers/) | Route handlers: parse/validate input, per-route gates via `RouteOptions` (auth, subdomain, rate limit, body parsers), call services, format responses. |
| **Drivers** | [src/backend/drivers/](src/backend/drivers/) | Optional RPC-style handlers on `/drivers/*`; thin shells that validate inputs and call services/stores. |
| **Services** | [src/backend/services/](src/backend/services/) | Business logic. Assume the caller is already authenticated/authorized. |
| **Stores** | [src/backend/stores/](src/backend/stores/) | Persistence; wraps clients in the domain shapes services consume. |
| **Clients** | [src/backend/clients/](src/backend/clients/) | Adapters for external/internal services (sql, redis, s3, email, event bus). Protocols, not domain concepts. |
| **Config** | `config.*.json` → `IConfig` | Flat, typed config object every layer receives at construction. |
A public API can be exposed through either a controller or a driver — both are supported; prefer a controller when you need fine-grained control over routes and gates. [doc/contributing-apis.md](doc/contributing-apis.md) has the decision guide with links to the decorators and middleware for each.
Cross-layer rules:
- **Don't reach across layers.** Controllers don't poke clients; services don't register routes. If you want to, the abstraction is wrong — fix the abstraction.
- **Don't call sideways within a layer for code reuse.** Two services needing the same logic means a util/helper, not a service-to-service dependency.
- **Prefer explicit arguments over `Context` (ALS).** Reach for [Context](src/backend/core/context.ts) only for genuinely request-scoped values that would otherwise thread through many layers — today mostly `actor` and `req`.
### Extensions
[extensions/](extensions/) parallels the layered stack and is for **non-crucial parts of the system** — Puter still works with the extension removed. If core needs to call it, it belongs in core, not an extension (see [whoami](extensions/whoami.ts) as the cautionary example). The `extension` global ([src/backend/extensions.ts](src/backend/extensions.ts)) exposes:
-`extension.registerClient/Store/Service/Driver/Controller(name, Class)` for first-class additions.
-`extension.on(event, handler)` and `extension.get/post/put/delete/patch/use(path, opts?, handler)` for the lightweight common case. `opts` is the same `RouteOptions` controllers use.
-`extension.import('client' | 'store' | 'service' | 'controller' | 'driver')` — lazy proxy to instantiated core objects. `extension.config` exposes live config.
Follow the same layered structure inside an extension — unless it only needs a few route handlers, in which case the lightweight helpers are enough on their own.
### Backend tests
- Vitest; test files sit next to the code they test (`*.test.ts` / `*.test.js`). Run with `npm run test:backend`.
- **Mock data, not methods.** Stub inputs (fixtures, fake rows, payloads), not the function under test or the layer beneath it — over-mocking produces tests that pass while production breaks. If you must mock, mock at a real boundary (a client/external service).
- **Prefer the test server over mocking deps.** `setupPuterTestEnv()` in [src/backend/testUtil.ts](src/backend/testUtil.ts) boots a fully in-memory backend; hit a real database/client shape where reasonable — integration shapes catch what mocked unit tests miss.
---
## puter.js (the SDK)
[src/puter-js/](src/puter-js/) is the public SDK. It ships live from `https://js.puter.com/v2/` with no version pinning — every existing app picks up changes immediately. Treat every observable behavior (signatures, response fields, error codes) as something a production app depends on.
Layout: SDK modules in [src/puter-js/src/modules/](src/puter-js/src/modules/) (one file or directory per area — `FileSystem/`, `kv/`, `ai/`, …), shared plumbing in [src/puter-js/src/lib/](src/puter-js/src/lib/), generated (gitignored) type declarations in `src/puter-js/types/`, API tests in [src/puter-js/tests/api/](src/puter-js/tests/api/), UI e2e tests in [src/puter-js/tests/e2e/](src/puter-js/tests/e2e/), developer docs in [src/docs/](src/docs/).
Language & typing: puter.js source is plain JavaScript — never TypeScript files — typed via JSDoc. **The JSDoc is the source of truth for the SDK's types.**`src/puter-js/types/` is `tsc --emitDeclarationOnly` output: the SDK build (`npm run build` in `src/puter-js`) generates it, the npm tarball ships it so TypeScript consumers get declarations, and git ignores it — it is never committed and never edited by hand. `npm run check:puterjs:types` generates it in CI and type-checks the result without `skipLibCheck`. The one hand-written declaration file is [src/puter-js/index.d.ts](src/puter-js/index.d.ts), which decides what is public and re-exports the generated names.
Declare a shape where it belongs, and reference it with an `import(...)` type from elsewhere. A shape more than one file needs lives in the module's `types.js`; a shape shared across modules lives in [src/puter-js/src/lib/types.js](src/puter-js/src/lib/types.js); a shape with one consumer can stay next to it:
Use `@typedef {Object}` + `@property` for any shape whose fields need documenting — it is the only JSDoc form that carries a doc comment per field into the generated declaration. Keep the inline object-literal form for small internal shapes with nothing to say about each field, and prefer `unknown` over `*`:
Public (exposed) methods must carry JSDoc types — parameters and return value — with one `@overload` block per accepted call form, since those overloads *are* the published signature. Unexposed/private helpers are typed at the contributor's discretion: annotate where it helps the next reader, and either way keep them clean. Members tagged `@internal` are stripped from the generated declarations, so use that tag rather than `@private` to keep something off the public surface.
Typing in JS files is encouraged: annotate with JSDoc `@type`/`@param`/`@returns` using the TypeScript type system, and define shared shapes with `@typedef`. API types must not be `unknown` or untyped `...args` — spell out the real parameter and return shapes; the only exception is values passed through transparently to an upstream layer that owns their type. For example:
```js
/**
* @typedef {{key:string, value: unknown}} KVEntry
*/
/** @type {KVEntry[]} */
letentries=[];
```
Every SDK change carries all five of the following — a puter.js PR missing one is incomplete:
1.**Backward compatibility.** Mandatory unless a maintainer explicitly signs off on a break. Existing call signatures keep working (including both positional and options-object forms where a method supports them); new parameters are optional with defaults that preserve old behavior; never rename or repurpose existing params, response fields, or error codes. New parameter names are `camelCase` (existing `snake_case` stays for compatibility). Say in the PR how existing callers are unaffected.
2.**Tests.** Add or extend a suite in [tests/api/suites/](src/puter-js/tests/api/suites/) (`<area>.suite.ts`; register new suites in `suites/index.ts` — no globbing). One suite runs unchanged on node, browser, and workerd via `npm run test:puterjs`; never write a per-platform test. The runners execute the **built** bundle — run `npm run build:workerLib` after SDK changes or the suite silently tests stale code. For `puter.ui.*` methods rendered by the desktop, use the Playwright e2e harness instead — see [src/puter-js/TESTING.md](src/puter-js/TESTING.md).
3.**Docs.** New or changed APIs update [src/docs/src/](src/docs/src/): the method page (`<Area>/<method>.md`, with frontmatter and a runnable example) and the area overview when the surface changes. Docs are the contract users code against — signatures, defaults, and return shapes must match the implementation exactly.
4.**Types.** Type the change in JSDoc on the implementation, then run `npm run check:puterjs:types` to confirm it still produces declarations that type-check. Name any new type in [src/puter-js/index.d.ts](src/puter-js/index.d.ts) if consumers should be able to import it. Declarations must match runtime behavior exactly — a wrong type is worse than a missing one.
5.**Error handling.** Reject/throw `{ message, code }` objects with stable `snake_case` codes, matching the existing modules (see `KV.js`). Validate cheap preconditions client-side before making the network call; pass backend errors through unchanged rather than swallowing or re-wrapping them. Error codes are API surface — changing one is a breaking change.
[doc/contributing-apis.md](doc/contributing-apis.md) walks the full lifecycle of adding an API across backend + SDK.
---
## GUI
[src/gui/](src/gui/) is the Puter desktop: deliberately plain JavaScript + jQuery with HTML-string templates. Don't introduce a UI framework or a new rendering pattern.
The guiding rule here is **conformity over novelty** — match the existing design and code structure even where you'd personally choose differently. A visually or structurally divergent addition is a defect even when it works.
- **Reuse existing UI primitives before writing new ones.** Windows and dialogs are `UIWindow*` functions in [src/gui/src/UI/](src/gui/src/UI/); generic pieces already exist (`UIAlert`, `UIPrompt`, `UIContextMenu`, `UINotification`, `UIPopover`, widgets in [UI/Components/](src/gui/src/UI/Components/)). A new window should read like its neighbors: an async function taking an options object, composing an HTML string, wiring behavior with jQuery, delegating to `UIWindow(...)`.
- **Match the visual language.** Use existing CSS classes (`button`, `button-primary`, window chrome, form styles) and copy the layout patterns of neighboring windows; new styles go in [src/gui/src/css/](src/gui/src/css/) following existing conventions. Verify anything positional (menus, overlays, z-index) on both desktop and mobile viewports.
- **i18n every user-facing string.** No hardcoded UI text — use `i18n('key')` and add the key to [src/gui/src/i18n/translations/en.js](src/gui/src/i18n/translations/en.js). Run `npm run check-translations` before opening the PR.
- **Shared logic goes in helpers/services.** Reusable non-UI logic belongs in [src/gui/src/helpers/](src/gui/src/helpers/) or [src/gui/src/services/](src/gui/src/services/), not copy-pasted between windows.
- **Tests.** Vitest is wired for the GUI (`src/**/*.test.js`; see [appOrder.test.js](src/gui/src/UI/Dashboard/appOrder.test.js) for the shape). Extract pure logic into functions and test those. Desktop behavior driven through puter.js (`puter.ui.*`) is covered by the Playwright harness in [src/puter-js/tests/e2e/](src/puter-js/tests/e2e/) — add a spec there when you change how the desktop renders SDK-driven UI.
We at **Puter** are committed to maintaining a secure experience for our users and community. We greatly value the contributions of security researchers and welcome responsible disclosure of security issues.
## Scope
The following are in scope for this program:
* **The Puter open-source project** (available at [github.com/HeyPuter/puter](https://github.com/HeyPuter/puter))
* **`puter.com`**
* **`api.puter.com`**
Out-of-scope:
* Third-party services, applications, or libraries not maintained by Puter.
* Social engineering attacks (e.g., phishing against staff).
* Denial of Service (DoS), spam, or volumetric attacks.
* Physical security issues.
## Known Non-Issues (Please Check Before Submitting)
The following have already been reviewed and determined **not to be vulnerabilities**. Reports that only re-describe one of these are **not eligible for a reward** and will be closed as non-issues — even if they include new code references. Please review this list before submitting:
* **XSS / CORS / token issues scoped to `api.puter.com`.** The primary user session cookie lives on `puter.com`, not on the API origin, so reflected/stored XSS, CORS, or token handling on `api.puter.com` is generally out of scope. Two exceptions we *do* evaluate on their merits — please report these: (a) attacker-controlled content served **inline** (e.g. an HTML `Content-Type`) from API file/response endpoints, and (b) anything that abuses the app-scoped `puter_token_v2` companion cookie set on the API host.
* **SSRF via `secureFetch`.** Production routes outbound requests through an isolated proxy that has no access to internal/SSRF-sensitive resources.
* **Attacks that depend on guessing an `appInstanceID` or app UID.** These are random 128-bit secret values and are not considered guessable.
* **Apps invoking drivers, creating workers, or using KV.** Applications are intended to do this; worker permissions are scoped to the owning app. This is by design.
* **App metadata or app user-count "leaks".** This information is currently public by design.
* **General "token in a URL" / token-lifetime designs** — signed directory URLs exposing children, a write signature implying read, or app tokens outliving a web session. These are current intended behaviors.
* **Missing PKCE, unverified `id_token` signatures, or other OIDC hardening.** Provider tokens are obtained through a server-to-server authorization-code exchange with the provider's token endpoint over TLS, so the resulting `id_token` / userinfo claims are trusted from that channel rather than from local JWKS signature verification — the callback never accepts a caller-supplied `id_token`. Adding local signature / `aud` / `iss` / `exp` checks is welcome defense-in-depth (please open a GitHub issue/PR), but their absence is not an account-takeover vector on its own.
* **JWT "algorithm confusion" / unpinned `algorithms` in `jwt.verify`.** Puter's session tokens are HMAC-signed (HS256) with a server-side secret, and there is no asymmetric public key anywhere in the verification path, so `alg`-substitution attacks do not apply. Explicitly pinning `algorithms` is a fine hardening PR, but it is not a vulnerability.
* **Static-source "SQL injection" in internal pagination.** Findings such as `LIMIT ${limit}` in list/notification queries: the limit is numerically coerced and clamped before it reaches the query, so it is not reachable with attacker-controlled string input. Hardening PRs to the internal stores are welcome, but these are not exploitable as reported.
* **Deprecated `saveTo*` GUI app messages.** The legacy `saveToDesktop` / `saveToDocuments` / etc. app-IPC handlers can create — never overwrite — new files in standard user folders. The behavior is non-destructive, path-traversal-safe, and deprecated (slated for removal); it is not treated as a sandbox escape.
* **Best-practice suggestions** such as login/registration username enumeration (kept intentionally for UX) or unauthenticated unsubscribe links (industry norm).
* **Rate-limiting suggestions for TURN credential issuance** (intentional; not billed per tunnel).
If you believe you have a **genuinely new** exploit chain that defeats one of these rationales (for example, demonstrating a sensitive credential that really is reachable on `api.puter.com`), say so explicitly and show why the reasoning above does not apply.
## Rules of Engagement
To participate, you must:
1.**Report responsibly**: Provide detailed steps to reproduce the issue, including proof-of-concept code, screenshots, or a screen recording (see *Proof of Reproduction* below).
2.**Do no harm**: Do not exfiltrate, modify, or delete data. Only access your own account or test data.
3.**Respect availability**: Do not perform denial-of-service attacks or automated scans that degrade service.
4.**Follow disclosure policy**: Do not publicly disclose vulnerabilities until we have confirmed and patched the issue.
5.**Act in good faith**: Make every effort to avoid privacy violations, destruction of data, and interruption or degradation of services.
6.**Check the Known Non-Issues list**: Reports matching an item in the "Known Non-Issues" section above are not eligible and will be closed.
Reports that do not meet these guidelines may not be eligible for a reward.
## Proof of Reproduction
Reports must demonstrate a **working, reproducible exploit with real impact** — not a theoretical or static-source-review finding. Please include:
* Exact steps to reproduce, the relevant request/response or code path, and the commit or version you tested.
* The **observed** result versus the **expected** result.
* For client-side, UI, or authentication-flow bugs: a short screen recording (≤ 2 minutes) showing the exploit working end-to-end on a real Puter instance.
* For server-side bugs: a runnable proof-of-concept.
Reports based solely on reading the source ("source review only, not tested") or unverified AI/LLM-generated reports are the **lowest triage priority and are generally not eligible**. If you used an AI tool to help find an issue, you must personally verify that it actually reproduces before submitting.
Please submit **one issue per report**. Bundled "audit packs" of many speculative findings will be declined; send each confirmed issue separately.
We aim to acknowledge receipt within **72 hours** and provide a resolution timeline.
## Reward Structure
We offer monetary rewards based on the severity of the vulnerability, as determined by our internal assessment (using CVSS as a guide).
* **Critical: \$1,000 – \$2,000**
* **High: \$500 – \$1,000**
* **Medium: \$200 – \$500**
* **Low: \$50 – \$100**
Non-security issues, suggestions, and best practices feedback are always welcome, but may not qualify for a reward.
If multiple researchers report the same issue, the bounty will be awarded to the first eligible report we receive.
## Payments Disclaimer
All reward amounts are **guidelines only**. Final decisions about eligibility, severity classification, and payout amount are made at the sole discretion of the Puter security team. We reserve the right to determine whether a report qualifies for a bounty, and whether any payment will be issued at all. Submitting a report does not guarantee compensation.
### Payment Method Requirement
At this time, **payments will only be made via PayPal**. To be eligible to receive a bounty, researchers must have a valid PayPal account capable of receiving payments. We are unable to process payments through other services or methods at this time.
## Legal Safe Harbor
If you make a good-faith effort to comply with this policy, we will consider your research to be authorized. If you inadvertently access data outside your own account, stop immediately and include details in your report so we can investigate and remediate.
- add option to disable temporary users ([f9333b3](https://github.com/HeyPuter/puter/commit/f9333b3d1e05bd0dffaecd2e29afd08ea61559fc))
- add some default groups ([ba50d0f](https://github.com/HeyPuter/puter/commit/ba50d0f96d58075abec067d24e6532bd874093f0))
- Add support for dropping multiple Puter items onto Dev Center (close #311) ([8e7306c](https://github.com/HeyPuter/puter/commit/8e7306c23be01ee6c31cdb4c99f2fb1f71a2247f))
- Update zhtw.js, Complete Traditional Chinese translation based on English file #550 ([b9e73b7](https://github.com/HeyPuter/puter/commit/b9e73b7288aebb14e6bbf1915743e9157fc950b1))
- update zhtw.js to match en.js ([37fd666](https://github.com/HeyPuter/puter/commit/37fd666a9a6788d5f0c59311499f29896b48bc82))
- Add Tamil translation to translations.js ([8a3d043](https://github.com/HeyPuter/puter/commit/8a3d0430f39f872b8a460c344cce652c340b700b))
- Move Tamil translation to the rest of translations ([333d6e3](https://github.com/HeyPuter/puter/commit/333d6e3b651e460caca04a896cbc8c175555b79b))
- Translation improvements, mainly style and context-based ([8bece96](https://github.com/HeyPuter/puter/commit/8bece96f6224a060d5b408e08c58865fadb8b79c))
- update translation file es.js to be up to date with the file en.js ([1515278](https://github.com/HeyPuter/puter/commit/151527825f1eb4b060aaf97feb7d18af4fcddbf2))
- Translate en.js as of 2024-07-10 ([8e297cd](https://github.com/HeyPuter/puter/commit/8e297cd7e30757073e2f96593c363a273b639466))
- Create hu.js hungarian language ([69a80ab](https://github.com/HeyPuter/puter/commit/69a80ab3d2c94ee43d96021c3bcbdab04a4b5dc6))
- Update translations.js to Hungarian lang ([56820cf](https://github.com/HeyPuter/puter/commit/56820cf6ee56ff810a6b495a281ccbb2e7f9d8fb))
- Tamil translation ([81781f8](https://github.com/HeyPuter/puter/commit/81781f80afc07cd1e6278906cdc68c8092fbfedf))
-`launch_app` not considering `explorer` as a special case ([98e6964](https://github.com/HeyPuter/puter/commit/98e69642d027a83975a0b2b825317213098bb689))
- well kinda (HOSTNAME in phoenix) ([7043b94](https://github.com/HeyPuter/puter/commit/7043b9400c63842c4c54d82724167666708d3119))
- it was github actions the entire time ([602a198](https://github.com/HeyPuter/puter/commit/602a19895c05b45a7d283470e7af3ae786be1bf2))
- fix CI attempt #7 ([614f2c5](https://github.com/HeyPuter/puter/commit/614f2c5061525f230ccd879bfb047434ac46a9ba))
- fix CI attempt #6 ([9d549b1](https://github.com/HeyPuter/puter/commit/9d549b192d149eac96c316ded645bf7c2e96153d))
- fix CI attempt #5 ([74adcdd](https://github.com/HeyPuter/puter/commit/74adcddc1d60e0a513408a0716ed2b301126225d))
- fix CI attempt #4 ([84b993b](https://github.com/HeyPuter/puter/commit/84b993bce913c3ad99127063bcfaae19331b199c))
- fix CI attempt #3 ([3bca973](https://github.com/HeyPuter/puter/commit/3bca973f5f4e65a2bd24c634c347fbd681a7458b))
- fix CI attempt #2 ([aebe89a](https://github.com/HeyPuter/puter/commit/aebe89a1acb070764551e8e89e325325ffbed8f9))
- run mocha within packages in monorepo ([58c199c](https://github.com/HeyPuter/puter/commit/58c199c15356ac087a04b16dd18e8fe0f1aea359))
- make webpack output not look like errors ([ad3d318](https://github.com/HeyPuter/puter/commit/ad3d318d07377c78c0429247225655e489b68be4))
- No scrollbar for session list ([45f131f](https://github.com/HeyPuter/puter/commit/45f131f8eaf94cf3951ca7ffeb6f311590233b8a))
- fix path issues under win32 platform ([d80f2fa](https://github.com/HeyPuter/puter/commit/d80f2fa847bfaef98dc8d482898f5c15f268e4bd))
- Added missing Romanian translations. ([8440f56](https://github.com/HeyPuter/puter/commit/8440f566b91c9eb4f01addcb850061e3fbe3afc7))
- Add 2FA Romanian translations ([473b651](https://github.com/HeyPuter/puter/commit/473b6512c697854e3f3badae1eb7b87742954da5))
- Add Japanese Translation ([47ec74f](https://github.com/HeyPuter/puter/commit/47ec74f0aa6adb3952e6460909029a4acb0c3039))
- Completing Italian translation based on English file ([f5a8ee1](https://github.com/HeyPuter/puter/commit/f5a8ee1c6ab950d62c90b6257791f026a508b4e4))
- Completing Italian translation based on English file. ([a96abb5](https://github.com/HeyPuter/puter/commit/a96abb5793528d0dc56d75f95d771e1dcf5960d1))
- Completing Arabic translation based on English file ([78a0ace](https://github.com/HeyPuter/puter/commit/78a0acea6980b6d491da4874edbd98e17c0d9577))
- Update Arabic translations in src/gui/src/i18n/translations/ar.js to match English version in src/gui/src/i18n/translations/en.js ([fe5be7f](https://github.com/HeyPuter/puter/commit/fe5be7f3cf7f336730137293ba86a637e8d8591d))
- Update Arabic translations in src/gui/src/i18n/translations/ar.js to match English version in src/gui/src/i18n/translations/en.js ([bffa192](https://github.com/HeyPuter/puter/commit/bffa192805216fc17045cd8d629f34784dca7f3f))
- remove flag that breaks puter-js webpack ([7aadae5](https://github.com/HeyPuter/puter/commit/7aadae58ce1a51f925bf64c3d65ac1fa6971b164))
- Improve `getMimeType` to remove trailing dot in the extension if preset ([535475b](https://github.com/HeyPuter/puter/commit/535475b3c36a37e3319ed067a24fb671790dcda3))
## 2.4.0 (2024-07-08)
### Features
* add (pt-br) translation for system settings. ([77211c4](https://github.com/HeyPuter/puter/commit/77211c4f71b0285fb3060f7e5c8d493b4d7c4f0c))
* **backend:** add script service ([30550fc](https://github.com/HeyPuter/puter/commit/30550fcddda18469735499546de502d29b85e2ad))
* **backend:** Add tab completion to server console command arguments ([fa81dca](https://github.com/HeyPuter/puter/commit/fa81dca9507b7fa0f82099b75f2ab89c865626ac))
* **backend:** Add tab-completion to server console command names ([e1e76c6](https://github.com/HeyPuter/puter/commit/e1e76c6be71fdeb3b6246307b626734d8dc26f86))
* **backend:** add tip of day ([2d8e624](https://github.com/HeyPuter/puter/commit/2d8e6240c61dc6301f49cbdcd1c3b04736f9ca93))
* **backend:** allow services to provide user properties ([522664d](https://github.com/HeyPuter/puter/commit/522664d415c33342500defec309c2ff15bc94804))
* **backend:** allow services to provide whoami values ([fccabf1](https://github.com/HeyPuter/puter/commit/fccabf1bc0c4418f3599222616dd63bf98c14fe1))
* **backend:** improve logger and reduce logs ([4bdad75](https://github.com/HeyPuter/puter/commit/4bdad75766d0617a164024b39b79bf5373c495a6))
* Display app icon and description in embeds ([ef298ce](https://github.com/HeyPuter/puter/commit/ef298ce3aa3ce90224e883fb0ba33f9cd3a3da44))
* get first test working on share-test service ([88d6bee](https://github.com/HeyPuter/puter/commit/88d6bee9546f36d689c53ec7fe95f01f772f5211))
* **git:** Add --color and --no-color options ([d6dd1a5](https://github.com/HeyPuter/puter/commit/d6dd1a5bb0a2b2bba2cfe86d2e51ff2a6e42841c))
* **git:** Add a --debug option, which sets the DEBUG global ([fa3df72](https://github.com/HeyPuter/puter/commit/fa3df72f6ed2d45a440ebc2aacbbae67bf042478))
* **git:** Add authentication to clone, fetch, and pull. ([364d580](https://github.com/HeyPuter/puter/commit/364d580ff896691ee70d3735f495c720651a9f41))
* **git:** Add diff display to `show` and `log` subcommands ([3cad1ec](https://github.com/HeyPuter/puter/commit/3cad1ec436f99a78f782ab9576325d4341284964))
* **git:** Add start-revision and file arguments to `git log` ([49c2f16](https://github.com/HeyPuter/puter/commit/49c2f163515d2130c17a6f6a6a16bc27ea69336a))
* **git:** Allow checking out a commit instead of a branch ([057b3ac](https://github.com/HeyPuter/puter/commit/057b3acf00af49c005b9bf7069c5d22983a32e1e))
* **git:** Color output for `git status` files ([bab5204](https://github.com/HeyPuter/puter/commit/bab5204209aa2efc0c053643677a78db6ede0929))
* **git:** Display file contents as a string for `git show FILE_OID` ([a680371](https://github.com/HeyPuter/puter/commit/a68037111a04580cfa2688694a68ef6ac7a495fa))
* **git:** Display ref names in `git log` and `git show` ([45cdfcb](https://github.com/HeyPuter/puter/commit/45cdfcb5bfa66937b33054a127e0b17001f3faa4))
* **git:** Format output closer to canonical git ([60976b1](https://github.com/HeyPuter/puter/commit/60976b1ed61984d9d290f3a0ae99dd97632e9909))
* **git:** Handle detached HEAD in `git status` and `git branch --list` ([2c9b1a3](https://github.com/HeyPuter/puter/commit/2c9b1a3ffc3d5e282ffe5b83a86314e99445bbc6))
* **git:** Make `git add` work for deleted files ([9551544](https://github.com/HeyPuter/puter/commit/955154468f48e45028dad2e916708d6a763affad))
* **git:** Make shorten_hash() guaranteed to produce a unique hash ([dd10a37](https://github.com/HeyPuter/puter/commit/dd10a377493c0d8f10a1ac8779dc27f3f3bf6c37))
* **git:** Resolve more forms of commit reference ([b6906bb](https://github.com/HeyPuter/puter/commit/b6906bbcaaa50fc8a8c60beb6d2d38bcb7dda758))
* **git:** Understand references like `HEAD^` and `main~3` ([711dbc0](https://github.com/HeyPuter/puter/commit/711dbc0d2fde9c2ddc6c86f64fb4caa7837c9dcb))
* implicit access from apps to shared appdata dirs ([31d4eb0](https://github.com/HeyPuter/puter/commit/31d4eb090efb340fdfb7cb6b751145e859624eeb))
* introduce notification selection via driver ([c5334b0](https://github.com/HeyPuter/puter/commit/c5334b0e19cf9762f536ec482c3ff872e9c12399))
* **phoenix:** Add --dump and --file options to sed ([f250f86](https://github.com/HeyPuter/puter/commit/f250f86446a506f24fa2ad396328e3a2212a68d0))
* **phoenix:** Add more commands to sed, including labels and branching ([306014a](https://github.com/HeyPuter/puter/commit/306014adc77a7ca155feb95d1146cb46ee075b52))
* **phoenix:** Expose parsed arg tokens to apps that request them ([4067c82](https://github.com/HeyPuter/puter/commit/4067c82486c99cad20f41927ad39ebea438b717f))
* **phoenix:** Implement an `exit` builtin ([3184d34](https://github.com/HeyPuter/puter/commit/3184d3482c7b95c0fd1fc0745555ff82fc9a8c99))
* **phoenix:** Implement parsing of sed scripts ([0d4f907](https://github.com/HeyPuter/puter/commit/0d4f907b6675b15bd50a55f50aa28f0803b18b7b))
* **phoenix:** Make `clear` clear scrollback unless `-x` is given ([75a989a](https://github.com/HeyPuter/puter/commit/75a989a7b69bfdfdf69e5f0365027c5b27d8bfc6))
* **Phoenix:** Pass command line arguments and ENV when launching apps ([8f1c4fc](https://github.com/HeyPuter/puter/commit/8f1c4fcda98e72a7b970e8c6fc2fe39a5e012264))
* **phoenix:** Respond to exit status codes ([5de3052](https://github.com/HeyPuter/puter/commit/5de305202656a172b187dac87543d6c1c69a2958))
* **phoenix:** Show actual host name in prompt and neofetch ([4539408](https://github.com/HeyPuter/puter/commit/4539408a218a50244dc615cf7de56c29dcac53e6))
* rate-limit for excessive groups ([4af279a](https://github.com/HeyPuter/puter/commit/4af279a72fc9de89ddc3ba51806ca3760a36265d))
* re-send unreads on login ([02fc4d8](https://github.com/HeyPuter/puter/commit/02fc4d86b7166fb4803be5d28e2a593d6b7d9785))
* register dev center to apps ([10f4d7d](https://github.com/HeyPuter/puter/commit/10f4d7d50ce9314f9c3888c74cb17c8ebbecee98))
* send notification when file gets shared ([2f6c428](https://github.com/HeyPuter/puter/commit/2f6c428a403a006f7878861d2f0356c3294519be))
* start directory index frame ([fb1e2f2](https://github.com/HeyPuter/puter/commit/fb1e2f21fb67aefe0602f6c978199c7cd019bbf7))
* support canonical puter.js url in dev ([fd41ae2](https://github.com/HeyPuter/puter/commit/fd41ae217c7a9f7229326f62a829471580a744bd))
* **ui:** add new components ([577bd59](https://github.com/HeyPuter/puter/commit/577bd59b6cc94810e851ad544f8234e25a4e6e27))
* **ui:** add new components ([38ba425](https://github.com/HeyPuter/puter/commit/38ba42575ce9f3506f8ce219b9580202b3ed9993))
* update share endpoint to support more things ([dd5fde5](https://github.com/HeyPuter/puter/commit/dd5fde5130c1840ab598e6622766ae835142e58a))
### Bug Fixes
* add app_uid param to kv interface ([f7a0549](https://github.com/HeyPuter/puter/commit/f7a054956b8739a3bc305a49faee929ea0da1e15))
* add missing columns for public directory update ([b10302a](https://github.com/HeyPuter/puter/commit/b10302ad744fd9c58f9735743e075815183c772c))
* Add missing file extension to 0009_app-prefix-fix.sql in DB init ([a8160a8](https://github.com/HeyPuter/puter/commit/a8160a8cdcdd6aff98728a6f1643d93386e6bb5a))
* add permission implicator for file modes ([e63ab3a](https://github.com/HeyPuter/puter/commit/e63ab3a67f6555eb13d6af477a8da9f1b54d6608))
* **backend:** remove a bad thing that really doesn't work ([8d22276](https://github.com/HeyPuter/puter/commit/8d22276f13106f7642d11da30b1500817a20ad43))
* bug introduced when refactoring /share to Sequence ([ecb9978](https://github.com/HeyPuter/puter/commit/ecb997885c1efb766827c84d2ffb8dc6ddabe992))
* check subdomain earlier for /apps ([4e3a24e](https://github.com/HeyPuter/puter/commit/4e3a24e6093e279e210765e07e436f4e63b74072))
* do not delegate to select on read like ever that is really dumb ([a2a10b9](https://github.com/HeyPuter/puter/commit/a2a10b94be59403e03fb08bec5d7c056ce5b554f))
* docker runtime fail because stdout columns ([94c0449](https://github.com/HeyPuter/puter/commit/94c0449437ce4cb26d00a15a3f277bc7b09367b4))
* fix issues with apps in /share endpoint ([0cf90ee](https://github.com/HeyPuter/puter/commit/0cf90ee39af6548d271dec45ed8ee9e6df1cd14d))
* fix owner ids for default apps ([283f409](https://github.com/HeyPuter/puter/commit/283f409a662d126e7f3ce811f1467ac6fab9a522))
* fix permission cascade properly this time ([de58866](https://github.com/HeyPuter/puter/commit/de5886698e1eae2b250baac174b57029f3244e96))
* Fix phoenix app prefix and TokenService test ([afb9d86](https://github.com/HeyPuter/puter/commit/afb9d866b5091058711db931cde904947e661c15))
* fix that fix ([b126b67](https://github.com/HeyPuter/puter/commit/b126b670940a0e20cfe7bd0eba3db891bab5c142))
* **git:** Make git commit display detached HEAD correctly ([73d0f5a](https://github.com/HeyPuter/puter/commit/73d0f5a90cb5dcbadfc6d0fd22f14e8bc0e61f86))
* group permission audit table ([7d2f6d2](https://github.com/HeyPuter/puter/commit/7d2f6d256f56e30d752e9999c6e8bde68f9d9637))
* handle subpaths under another user ([d128cee](https://github.com/HeyPuter/puter/commit/d128ceed6f4928fa0793815feb2e2715cd273ff8))
* handling of batch requests with zero files ([c0063a8](https://github.com/HeyPuter/puter/commit/c0063a871fd891a1774f1bee00e86170fed249fa))
* i forgot to test reloading ([7eabb43](https://github.com/HeyPuter/puter/commit/7eabb43bd4257b4129d67eaeda2aa27e8268dc78))
* improve console experience on mac ([15465bf](https://github.com/HeyPuter/puter/commit/15465bfc5035a64762f7c86a3d38af8be6be5b59))
* incorrect error from suggested_apps ([b648817](https://github.com/HeyPuter/puter/commit/b648817f2743c2b6214ebe4177d921c9b9027594))
* Make polyfilled import.meta.filename getter a valid function ([85c6798](https://github.com/HeyPuter/puter/commit/85c679844869b6b05fcbda231d8dc7026a66da97))
* null email in request to /share ([bf63144](https://github.com/HeyPuter/puter/commit/bf63144f7a79c48bd650ae851ddd0c8a10d748c3))
* Only run Component initialization functions once ([5b43358](https://github.com/HeyPuter/puter/commit/5b43358219402bee3eadf4a0f184a4b924d3293b))
* **parsely:** Make Repeat parser work when no separator is given ([9b4d16f](https://github.com/HeyPuter/puter/commit/9b4d16fbe9d5698c57f9da725a22b528a7d7cac2))
* **phoenix:** Add missing newlines to sed command output ([e047b0b](https://github.com/HeyPuter/puter/commit/e047b0bf302284da61e677432e4cc25b531b24f2))
* **phoenix:** Gracefully handle completing a non-existent path ([d76e713](https://github.com/HeyPuter/puter/commit/d76e7130cba9f0ca05940abafe4fd1a41464aa83))
* property validation on some permission endpoints ([0855f2b](https://github.com/HeyPuter/puter/commit/0855f2b36eca3bbdaa8429cbde3aa1242e8e96ee))
* readdir on file ([a72ec97](https://github.com/HeyPuter/puter/commit/a72ec9799ac3bd76ceafa22cce149e373a13f3b9))
* remove last component when share URL is file ([1166e69](https://github.com/HeyPuter/puter/commit/1166e69c76688d1811701c56cd4df9d38e286793))
* remove legacy permission check in stat ([f2c6e01](https://github.com/HeyPuter/puter/commit/f2c6e01296e4214336e63bc2d69bcbf17f59890f))
* Remove null or duplicate app entries from suggest_app_for_fsentry() ([6900233](https://github.com/HeyPuter/puter/commit/6900233c5aaa2d1a49f495e9f9a060796757a91e))
* **security:** Move token for socket.io to request body ([49b257e](https://github.com/HeyPuter/puter/commit/49b257ecffbb1e12090b86a67528a5ad09da69db))
* switch share notif username to sender ([cd65217](https://github.com/HeyPuter/puter/commit/cd65217f5cda1c986ee231e2eeeef5abefa36ecb))
* **Terminal:** Accept input from Chrome on Android ([4ef3e53](https://github.com/HeyPuter/puter/commit/4ef3e53de34f0097950a7e707ca2483863beafb5))
* Throw an error when readdir is called on a non-directory ([46eb4ed](https://github.com/HeyPuter/puter/commit/46eb4ed2b96c235e10e15645a30d2f192a1af0de))
* type error in puter-site ([d96f924](https://github.com/HeyPuter/puter/commit/d96f924cad7a13ea6e9084bb0ebb79ecc5fcb8a3))
* ui color input attributes ([d9c4fbb](https://github.com/HeyPuter/puter/commit/d9c4fbbd1dcce12ee05ee33652a5fa518196463d))
* **ui:** improve Component base class ([f8780d0](https://github.com/HeyPuter/puter/commit/f8780d032b10138851c22af53b8610c578139acc))
* **security:** add ip rate limiting ([ccf1afc](https://github.com/HeyPuter/puter/commit/ccf1afc93c24ee7f9a126216209a185d6b4d9fe4))
* Show "Deleting /foo" in progress window when deleting files ([f07c13a](https://github.com/HeyPuter/puter/commit/f07c13a50cee790eec44bce2f6e56fbcbf73f9b0))
### Bug Fixes
* Add missing file extension to 0009_app-prefix-fix.sql in DB init ([a8160a8](https://github.com/HeyPuter/puter/commit/a8160a8cdcdd6aff98728a6f1643d93386e6bb5a))
* Add missing TextEncoder to PTT ([8d4a1e0](https://github.com/HeyPuter/puter/commit/8d4a1e0ed3872e2c82b9e4be9b6d8b359e9cea09))
* fix error handling for outdated node versions ([4c1d5a4](https://github.com/HeyPuter/puter/commit/4c1d5a4b6d009ce075897d499d3517219bd745a4))
* Fix phoenix app prefix and TokenService test ([afb9d86](https://github.com/HeyPuter/puter/commit/afb9d866b5091058711db931cde904947e661c15))
* increase QR code size ([d2de46e](https://github.com/HeyPuter/puter/commit/d2de46edfbc05d132d5c929f6935b82515fbbda0))
* Make PathCommandProvider reject queries with path separators ([d733119](https://github.com/HeyPuter/puter/commit/d73311945610417a1ebc7bb0723ced0a599594b4))
* Make url variable accessible to all users of it ([2f30ae7](https://github.com/HeyPuter/puter/commit/2f30ae7a825adcd8da95888c38fe39c34acee0ff))
* Only run Component initialization functions once ([5b43358](https://github.com/HeyPuter/puter/commit/5b43358219402bee3eadf4a0f184a4b924d3293b))
* Remove null or duplicate app entries from suggest_app_for_fsentry() ([6900233](https://github.com/HeyPuter/puter/commit/6900233c5aaa2d1a49f495e9f9a060796757a91e))
* **security:** always use application/octet-stream ([74e213a](https://github.com/HeyPuter/puter/commit/74e213a534dbf2844c8cebeee7eb59ec70de306e))
* **security:** Move token for socket.io to request body ([49b257e](https://github.com/HeyPuter/puter/commit/49b257ecffbb1e12090b86a67528a5ad09da69db))
Welcome to Puter, the open-source distributed internet operating system. We're excited to have you contribute to our project, whether you're reporting bugs, suggesting new features, or contributing code. This guide will help you get started with contributing to Puter in different ways.
Thanks for contributing. These rules aren't strictly enforced — but following them makes every PR easier. If anything's unclear, ping a core maintainer or open the PR and ask.
<br>
New to the backend? Start with [doc/architecture.md](doc/architecture.md).
# Report bugs
---
Before reporting a bug, please check our [the issues on our GitHub repository](https://github.com/HeyPuter/puter/issues) to see if the bug has already been reported. If it has, you can add a comment to the existing issue with any additional information you have.
## 1. Test it. Run it.
If you find a new bug in Puter, please [open an issue on our GitHub repository](https://github.com/HeyPuter/puter/issues/new). We'll do our best to address the issue as soon as possible. When reporting a bug, please include as much information as possible, including:
Run the affected code path end-to-end before opening a PR. "It builds" is not "it works."
- A clear and descriptive title
- A description of the issue
- Steps to reproduce the bug
- Expected behavior
- Actual behavior
- Screenshots, if applicable
- Your host operating system and browser
- Your Puter version, location, ...
Add tests for new behavior, endpoints, or bug fixes. If something's genuinely hard to test, say so in the PR.
Please open a separate issue for each bug you find.
## 2. Follow existing patterns
Maintainers will apply the appropriate labels to your issue.
Match the shape of similar code already in the repo. [doc/architecture.md](doc/architecture.md) is the source of truth for layers, wiring, and naming. If you think a pattern is wrong, raise it — don't quietly diverge.
<br>
In plain-JS files, typing is encouraged via JSDoc `@type` annotations using the TypeScript type system, with `@typedef` for shared shapes. Don't type API surfaces as `unknown` or untyped `...args` unless the values are passed through transparently to an upstream layer that owns their type.
# Suggest new features
## 3. Don't expose system or user information
If you have an idea for a new feature in Puter, please open a new discussion thread on our [GitHub repository](https://github.com/HeyPuter/puter/discussions) to discuss your idea with the community. We'll do our best to respond to your suggestion as soon as possible.
Scan your diff for stray logs, debug routes, internal paths, secrets, tokens, or user data in errors/responses. When in doubt, return less. Flag any auth, permission, or data-export changes in the PR description.
When suggesting a new feature, please include as much information as possible, including:
For private security reports, see [SECURITY.md](SECURITY.md).
- A clear and descriptive title
- A description of the feature
- The problem the feature will solve
- Any relevant screenshots or mockups
- Any relevant links or resources
## 4. AI-assisted code is fine — understood code is required
<br>
Don't commit code you couldn't have written, debugged, or defended yourself. Read the diff, run it, and be ready to explain it in review.
# Contribute code
## 5. Adding or changing APIs
If you'd like to contribute code to Puter, you need to fork the project and submit a pull request. If this is your first time contributing to an open-source project, we recommend reading this short guide by GitHub on [how to contribute to a project](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project).
If you add or change a public API (an endpoint, driver method, or puter-js method), follow [doc/contributing-apis.md](doc/contributing-apis.md) — backward compatibility, [developer docs](src/docs/), types, and tests all move in the same PR.
We'll review your pull request and work with you to get your changes merged into the project.
We maintain a list of issues that are good for first-time contributors. You can find these issues by searching for the [`good first issue`](https://github.com/HeyPuter/puter/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label in our [GitHub repository](https://github.com/HeyPuter/puter). These issues are designed to be relatively easy to fix, and we're happy to help you get started. Pick an issue that interests you, and leave a comment on the issue to let us know you're working on it.
Fix the typo, the dead import, the missing test, the bit you had to read twice. Keep cleanup proportional to the change — no refactors riding along on bug fixes.
<br>
---
## Documentation for Contributors
## Opening a PR
See [doc/contributors/index.md](./doc/contributors/index.md) for more information.
- One thing per PR where possible.
- Describe **what** and **why**; the diff shows how.
- Mention how you tested user-visible changes.
- Drafts welcome.
<br>
---
## Commit Messages
Use the imperative, as is the convention in the Linux kernel:
- correct: `Improve performance of readdir`
- incorrect: `Improved readdir`
- incorrect: `Improving readdir`
We have the following exceptions to this rule:
- If the commit message is in _past tense_, it's a shorthand for the following:
-`Apply changes that would be applied after one had <past tense message>`
- If the commit message is in _present tense_, it's shorthand for the following:
-`Apply changes that would be applied after <present-tense message>`
For example, the following are correct:
-`Improved readdir`
- interpret this as: `Apply changes that would be applied after one had improved readdir`
-`Improving readdir`
- interpret this as: `Apply changes that would be applied after improving readdir`
<br>
## Code Review
Once you've submitted your pull request, the project maintainers will review your changes. We may suggest some changes or improvements. This is a normal part of the process, and your contributions are greatly appreciated!
<br>
## Contribution License Agreement (CLA)
Like many open source projects, we require contributors to sign a Contribution License Agreement (CLA) before we can accept your code. When you open a pull request for the first time, a bot will automatically add a comment with a link to the CLA. You can sign the CLA electronically by following the link and filling out the form.
<br>
# Getting Help
If you have any questions about Puter, please feel free to reach out to us through the following channels:
<h3 align="center"><img width="80" alt="Puter.com, The Personal Cloud Computer: All your files, apps, and games in one place accessible from anywhere at any time." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">The Internet OS! Free, Open-Source, and Self-Hostable.</h3>
<h3 align="center">The Open-Source Internet Computer!</h3>
<p align="center">
<a href="https://puter.com/"><strong>« LIVE DEMO »</strong></a>
<a href="https://puter.com/?ref=github.com"><strong>« LIVE DEMO »</strong></a>
Puter is an advanced, open-source internet operating system designed to be feature-rich, exceptionally fast, and highly extensible. Puter can be used as:
Puter is an advanced, open-source, self-hostable internet computer designed to be feature-rich, fast, and highly extensible.
- A privacy-first personal cloud to keep all your files, apps, and games in one secure place, accessible from anywhere at any time.
- A platform for building and publishing websites, web apps, and games.
- An alternative to Dropbox, Google Drive, OneDrive, etc. with a fresh interface and powerful features.
- A remote desktop environment for servers and workstations.
- A friendly, open-source project and community to learn about web development, cloud computing, distributed systems, and much more!
### For Users
Puter's goal is to provide you with every app and feature you need to work, create, and play under one roof. From a simple [Notepad](https://online-notepad.com) and [Voice Recorder](https://voice-recorder.com) to [Spreadsheet](https://apps.puter.com/app/spreadsheet) and [Camera](https://online-camera.com), Puter wants to be the all-in-one solution for your digital life.
### For Developers
Puter provides everything you need to build and publish web apps and games. From [AI](https://developer.puter.com/ai/) to [Cloud Storage](https://developer.puter.com/object-storage/) and [Database](https://developer.puter.com/key-value-database/) to [Serverless Workers](https://developer.puter.com/serverless-workers/), Puter has you covered. Puter also helps you get users! Once you build your app, you can publish it on our [App Store](https://apps.puter.com/) to reach and monetize users.
<br/>
@@ -46,44 +48,27 @@ cd puter
npm install
npm start
```
**→** This should launch Puter at http://puter.localhost:4100
This will launch Puter at http://localhost:4000 (or the next available port).
We are always happy to help you with any questions you may have. Don't hesitate to ask!
<br/>
## License
## License
This repository, including all its contents, sub-projects, modules, and components, is licensed under [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) unless explicitly stated otherwise. Third-party libraries included in this repository may be subject to their own licenses.
<img src="https://puter.com/images/logo.png" alt="Puter Technologies Inc. Logo" width="150"/>
<br>
This trademark policy was prepared to help you understand how to use the Puter trademarks, service marks and logos with Puter Technologies Inc.'s Puter software.
While some of our software is available under a free and open source software license, that copyright license does not include a license to use our trademark, and this Policy is intended to explain how to use our marks consistent with background law and community expectation.
This Policy covers:
1. Our **word** trademarks and service marks: Puter Technologies Inc., Puter, Puter.com
2. Our **logos**: The Puter Technologies Inc. logo at the top of this policy
This policy encompasses all trademarks and service marks, whether they are registered or not.
<br>
<br>
## 1. GENERAL GUIDELINES
Whenever you use one of our marks, you must always do so in a way that does not mislead anyone about what they are getting and from whom. For example, you cannot say you are providing the Puter software when you're providing a modified version of it, because recipients may not understand the differences between your modified versions and our own.
You also cannot use our logo on your website in a way that suggests that your website is an official website or that we endorse your website.
You can, though, say you like the Puter software, that you participate in the Puter community, that you are providing an unmodified version of the Puter software.
You may not use or register our marks, or variations of them as part of your own trademark, service mark, domain name, company name, trade name, product name or service name.
Trademark law does not allow your use of names or trademarks that are too similar to ours. You therefore may not use an obvious variation of any of our marks or any phonetic equivalent, foreign language equivalent, takeoff, or abbreviation for a similar or compatible product or service. We would consider the following too similar to one of our Marks:
- MyPuter
- PuterFooBar
<br>
<br>
## 2. ACCEPTABLE USES
<br>
### Distribution of Unmodified Software
When you redistribute an unmodified copy of Puter software, you must retain all trademarks, logos, and notices we have placed on the software to identify its origin. This includes:
* Binary distributions exactly as we provide them
* Source code distributions exactly as we provide them
* Documentation and other materials directly from our official repositories
<br>
### Distribution of Modified Software
If you distribute a modified version of Puter software, you:
* Must remove all Puter logos from the modified software
* May use our word marks (but not logos) to accurately describe the software's origin
* Must clearly indicate that the software has been modified
* Must include a notice stating: "This software is a modified version of Puter software and is not endorsed by Puter Technologies Inc."
Example of acceptable description: "This software is derived from Puter software and includes modifications for [describe your changes]."
<br>
### Compatibility Statements
You may use our word marks (but not logos) to accurately describe your software's compatibility with Puter software under these conditions:
* Your statements about compatibility must be accurate and not misleading
* You must include the following notice: "Puter is a trademark of Puter Technologies Inc. This [product/service] is not affiliated with or endorsed by Puter Technologies Inc."
* You may not suggest that Puter Technologies Inc. has certified or approved your software
<br>
### Products Built for Puter
You may describe your product as working with or being built for Puter if:
* Your product is fully compatible with the documented Puter APIs
* Your product name follows this format: "[Your Product Name] for Puter"
* You include this notice in all materials: "Puter is a trademark of Puter Technologies Inc. [Your Product Name] is not affiliated with or endorsed by Puter Technologies Inc."
* Your branding and marketing materials do not create confusion about the source of your product
<br>
### Open Source Projects
For open source projects that interact with or extend Puter software:
* You may use "puter" as part of your project name only if:
* The name is in the format "[descriptor]-puter" (e.g., "auth-puter", "backup-puter")
* The project's README clearly states it's not officially associated with Puter
* The project maintains compatibility with current Puter APIs
* You must not use our logos without explicit permission
* You must include appropriate trademark attribution notices
<br>
### Community Activities
You may use our word marks (but not logos) for non-commercial community activities:
* User groups and meetups focused on Puter software
* Educational content about Puter software
* Blog posts, videos, articles, or tutorials about Puter software
Conditions for community use:
* Activities must be non-commercial
* Any fees charged must only cover actual costs
* You must include appropriate trademark attribution
* You must not suggest official endorsement without explicit permission
<br>
### Merchandise and Promotional Items
You may not create merchandise or promotional items bearing our marks without explicit written permission from Puter Technologies Inc.
<br>
### Academic and Research Use
You may use our word marks (but not logos) in:
* Academic papers
* Research publications
* Technical documentation
* Educational materials
Include appropriate citations and trademark attributions in such uses.
<br>
### Online Content and Social Media
When using our marks in online content:
* You may use our word marks in hashtags, handles, or usernames if:
* The content is clearly about Puter software
* You don't imply official status
* You include appropriate trademark attribution
* You must not register social media accounts that could be confused with official Puter accounts
<br>
### APIs and Development
When developing with Puter APIs:
* You may use our word marks to accurately describe your integration
* You must not use our marks in a way that suggests your API or service is endorse by
Puter or provided by Puter
* You must include appropriate trademark attribution
All uses described above must also comply with the General Guidelines section of this policy and maintain the integrity of our marks as described in the How to Display Our Marks section.
<br>
### No Domain Names
You must not register any domain that includes our word marks or any variant or combination of them.
<br>
<br>
## 3. HOW TO DISPLAY OUR MARKS
When you have the right to use our mark, here is how to display it.
<br>
### Trademark marking and legends
The first or most prominent mention of a mark on a webpage, document, or documentation should be accompanied by a symbol indicating whether the mark is a registered trademark ("®") or an unregistered trademark ("™"). If you don't know which applies, contact us.
Place the following notice at the foot of the page where you have used the mark: "Puter is trademark of Puter Technologies Inc."
<br>
### Use of trademarks in text
**Always use trademarks in their exact form with the correct spelling, neither abbreviated, hyphenated, or combined with any other word or words.**
Unacceptable: Puter-DB
**Don't pluralize a trademark.**
Unacceptable: I have seventeen Puters running in my lab.
**Always use a trademark as an adjective modifying a noun.**
Unacceptable: This is a Puter.
Acceptable: This is a Puter software application.
<br>
### Use of Logos
You may not change any logo except to scale it. This means you may not add decorative elements, change the colors, change the proportions, distort it, add elements, or combine it with other logos.
However, when the context requires the use of black-and-white graphics and the logo is color, you may reproduce the logo in a manner that produces a black-and-white image.
<br>
<br>
## This Document
These guidelines are based on the Model Trademark Guidelines, available at http://www.modeltrademarkguidelines.org., used under a Creative Commons Attribution 3.0 Unported license: https://creativecommons.org/licenses/by/3.0/deed.en_US.
Loosely inspired by the Controller–Service–Repository pattern with dependency injection. The backend is organized as a stack of layers where each layer only depends on the layers beneath it, and `PuterServer` ([src/backend/server.ts](../src/backend/server.ts)) instantiates each layer in order and hands the instances down to the next.
DRV["Drivers (optional) — RPC handlers on /drivers/*"]
SVC["Services — business logic, no auth"]
STR["Stores — persistence / domain shapes"]
CLI["Clients — sql, redis, s3, dynamo, email, …"]
CFG["Config — IConfig"]
REQ --> CTRL
CTRL --> DRV
DRV --> SVC
SVC --> STR
STR --> CLI
CLI --> CFG
style REQ fill:#0ea5e9,stroke:#0369a1,color:#fff
style CTRL fill:#1d4ed8,stroke:#1e3a8a,color:#fff
style DRV fill:#2563eb,stroke:#1e40af,color:#fff
style SVC fill:#4f46e5,stroke:#3730a3,color:#fff
style STR fill:#7c3aed,stroke:#5b21b6,color:#fff
style CLI fill:#9333ea,stroke:#6b21a8,color:#fff
style CFG fill:#334155,stroke:#1e293b,color:#fff
```
Each layer only depends on the layers beneath it, and every dependency is injected through the constructor by `PuterServer`. Extensions sit alongside this stack and can register into any layer — see [Extensions](#extensions) below.
| Layer | Lives in | Responsibility |
| --- | --- | --- |
| **Controllers** | [src/backend/controllers/](../src/backend/controllers/) | Route handlers. Parse + validate input, apply per-route gates (auth, subdomain, rate limit, body parsers — see `RouteOptions`), call into services, format responses. |
| **Drivers** | [src/backend/drivers/](../src/backend/drivers/) | Optional. RPC-style handlers exposed over the `/drivers/*` surface (`puter-kvstore`, `puter-chat-completion`, …). A driver is a thin shell that validates RPC inputs and calls into services/stores; controllers can hold a typed reference to drivers when they need the same logic over HTTP. |
| **Services** | [src/backend/services/](../src/backend/services/) | Business logic. Assume the caller is already authenticated/authorized — services do not run auth gates themselves. |
| **Stores** | [src/backend/stores/](../src/backend/stores/) | Persistence and storage logic. Wraps clients with the domain shape services consume (rows, entities, KV namespaces). |
| **Config** | `config.*.json` → `IConfig` | The flat, typed config object every layer receives at construction. |
Each layer receives the layers beneath it through its constructor, so dependencies are explicit and traceable from `PuterServer`. A controller does not reach into a client directly; if it needs one, the right move is usually a service.
## Entry point: `PuterServer`
`PuterServer` is the bootstrap. It:
1. Loads any configured extension directories (`config.extensions`) so extensions can register before instantiation begins.
2. Instantiates each layer in order — clients → stores → services → drivers → controllers — merging in anything extensions have registered for that layer.
3. Wires global middleware, mounts controller routes through `PuterRouter` (which translates `RouteOptions` into the gate/parser middleware chain), and mounts extension routes through the same materializer.
4. Fires `onServerStart` hooks across every layer once HTTP is listening, and `onServerPrepareShutdown` / `onServerShutdown` on the way down.
## Context (ALS)
We use [`Context`](../src/backend/core/context.ts) — backed by `AsyncLocalStorage` — to carry per-request state without threading it through every function signature. It is used **sparingly**, mostly for `actor` and `req`. The request-context middleware opens a scope per request after the auth probe runs; anything inside a request handler can call `Context.get('actor')` / `Context.get('req')` instead of plumbing it as an argument.
Prefer explicit arguments. Reach for `Context` only when the value is truly request-scoped and would otherwise need to thread through many layers.
## Extensions
Extensions live alongside core ([packages/puter/extensions/](../extensions/)) and parallel the layered stack. They are meant for **non-crucial parts of the system** — things Puter still works without if removed.
- **Good extensions**: [thumbnails](../extensions/thumbnails.ts), [serverInfo](../extensions/serverInfo.ts), [devWatcher](../extensions/devWatcher.ts) — opt-in features cleanly bolted on.
- **Should probably be core**: [metering](../extensions/metering.ts), [appTelemetry](../extensions/appTelemetry.ts) — clients now expect these to be present, so the "extension" framing is misleading.
- **Shouldn't have been an extension**: [whoami](../extensions/whoami.ts) — it's load-bearing for every authenticated client. Keep this one in mind as a cautionary example when deciding whether something belongs in an extension.
### Extension API
The `extension` global ([src/backend/extensions.ts](../src/backend/extensions.ts)) exposes:
- **Layer registration** for first-class additions:
- **Lightweight wrappers** for the common case where a full class isn't worth it:
-`extension.on(event, handler)` — subscribe to event-bus events.
-`extension.get(path, opts?, handler)` / `.post` / `.put` / `.delete` / `.patch` / `.head` / `.options` / `.all` / `.use` — register routes. The `opts` shape is the same `RouteOptions` controllers use, so `subdomain`, `requireAuth`, `adminOnly`, body parsers, etc. all work identically.
- **Cross-layer access**: `extension.import('client' | 'store' | 'service' | 'controller' | 'driver')` returns a lazy proxy to instantiated objects, and `extension.config` exposes the live config.
- **TypeScript preferred** in new code where feasible. Existing JS is fine; convert opportunistically when you're already touching a file.
- **`camelCase`** for variable/function names; **`PascalCase`** for classes and for files that contain a class (`AuthService.ts`, `KVStoreDriver.ts`).
- **Deduplicate**. If two services need the same logic, lift it into a util/helper rather than calling sideways across the same layer — services should not depend on other services for code reuse.
- **Don't reach across layers.** Controllers do not poke clients directly; services do not register routes. If you find yourself wanting to, that's usually a signal the abstraction is wrong.
How to add a new public API to Puter, and how to maintain one that already exists. "Public API" means anything applications can call: HTTP endpoints, `/drivers/*` methods, and the puter.js methods that wrap them. This guide is written for human contributors and AI agents alike — [AGENTS.md](../AGENTS.md) defers to it for API work.
puter.js is served live from `https://js.puter.com/v2/` with no version pinning — every app in existence picks up your change the moment it deploys, and the backend endpoints underneath have the same property. Assume every observable behavior (parameter handling, response fields, error codes, ordering) has someone depending on it.
Every change is backward compatible unless a maintainer has explicitly agreed to a break beforehand.
## Core or extension?
The first decision is where the API lives.
If the API is **not crucial to core functionality — nothing in core will call it — prefer an extension** over wiring it into core. Extensions live in [extensions/](../extensions/), parallel the core layered stack, and reach core through `extension.import(...)`:
extension.post('/my-feature/frobnicate',opts,handler);// or plain routes
```
Follow the same layered structure inside the extension (driver/controller → service → store) unless it genuinely only needs a couple of route handlers — then the lightweight `extension.get/post/...` helpers are enough on their own.
The test is the direction of dependency: Puter must still work with the extension removed. The moment core needs to call your API, it belongs in core — see [whoami](../extensions/whoami.ts) for the cautionary example of a load-bearing "extension".
## Adding a new API
Work through all seven steps; the PR is complete when every one is.
### 1. Design the surface first
- Sketch the signature, options, return shape, and error cases before writing code. Find the two or three most similar existing APIs and match their conventions.
- New parameter and field names are `camelCase`. (Existing `snake_case` names stay where they already exist.)
- Anything returning a list follows the [pagination convention](pagination.md): `limit`/`cursor` in, `{ items, cursor, total }` envelope out.
- Prefer an options object over a growing list of positional parameters, but keep the common case callable with a single argument where siblings do.
### 2. Backend
Follow the layered stack ([architecture.md](architecture.md)): a controller or driver at the edge, business logic in a service, persistence in a store. The edge parses and validates input and applies gates; services assume the caller is already authorized. Return exactly what the caller needs and no more — every response field you ship is permanent — and use stable `snake_case` error codes.
#### Controller or driver?
Both are supported ways to define an API. **Prefer a controller when you need fine-grained control** — URL shape, HTTP verbs, per-route gates, response and streaming formats. A **driver** fits when the API is a set of RPC methods implementing one of the named interfaces on `/drivers/*` (`puter-kvstore`, `puter-chat-completion`, …) and the generic driver plumbing — one call envelope, interchangeable implementations, per-method policies — covers what you need.
- **Controller:** extend `PuterController` ([src/backend/controllers/types.ts](../src/backend/controllers/types.ts)) and declare routes with the `@Controller(prefix)` class decorator plus `@Get`/`@Post`/… method decorators ([src/backend/core/http/decorators.ts](../src/backend/core/http/decorators.ts)), each taking `(path, routeOptions)` — or override `registerRoutes(router)` imperatively. Core controllers register in [src/backend/controllers/index.ts](../src/backend/controllers/index.ts); extensions use `extension.registerController(...)` or the plain route helpers.
- **Driver:** extend `PuterDriver` ([src/backend/drivers/types.ts](../src/backend/drivers/types.ts)) and mark it with the `@Driver(interfaceName, opts)` decorator ([src/backend/drivers/decorators.ts](../src/backend/drivers/decorators.ts)), which also declares its policies. Core drivers register in [src/backend/drivers/index.ts](../src/backend/drivers/index.ts); extensions use `extension.registerDriver(...)`.
#### Middleware and gates
- **Controller routes** (and extension routes — same options) take [`RouteOptions`](../src/backend/core/http/types.ts): auth gates (`requireAuth`, `requireUserActor`, `noUserSession`, `adminOnly`, `allowedAppIds`, and the access-token controls), `subdomain` routing, per-route `rateLimit`, body parsers, and arbitrary extra `middleware`. The auth flavors are subtle and default-deny — read the JSDoc on each field before picking.
- **Driver methods** get their policies from the `@Driver` options: per-method `rateLimit` (limit/window/backend), `concurrent` in-flight caps (optionally `bySubscription`), and `noUserSession`. The `/drivers/call` surface enforces them.
- **An endpoint that spends metered resources** on the caller's behalf — moving file content, making object-store requests, anything else the account is billed for — also declares `requireCredits: true`, which turns an account with nothing left of its budget away with a 402 before the handler runs. Endpoints that only describe or delete things deliberately don't: an account that has run out still has to be able to see what it has, clear it, and reach its billing pages. Drivers have no route options to declare this on, so they call `assertActorHasCredits` themselves ([src/backend/services/metering/enforcement.ts](../src/backend/services/metering/enforcement.ts)) — see `KVStoreDriver`, which does it once for every method.
### 3. puter.js
- Add the method to the matching module in [src/puter-js/src/modules/](../src/puter-js/src/modules/), matching the calling conventions of its siblings (promise-returning; positional shortcut plus options form where that's the local pattern).
- Validate cheap preconditions client-side and throw `{ message, code }` objects; pass backend errors through unchanged rather than swallowing or re-wrapping them.
### 4. Types
- Type the method where you wrote it, in JSDoc: `@param`/`@returns` on the implementation, one `@overload` block per accepted call form, and `@typedef {Object}` + `@property` for any new shape. The JSDoc is the source of truth — declarations are generated from it, so there is nothing to keep in sync by hand.
- Put a shape more than one file needs in the module's `types.js` (e.g. [src/puter-js/src/modules/kv/types.js](../src/puter-js/src/modules/kv/types.js)); anything shared across modules goes in [src/puter-js/src/lib/types.js](../src/puter-js/src/lib/types.js). A shape with one consumer can stay next to it.
- Run `npm run check:puterjs:types` — it generates the declarations and type-checks the published surface without `skipLibCheck`. **Never edit anything under `src/puter-js/types/`**: it is gitignored build output, produced by the SDK build and shipped in the npm tarball, and the next build overwrites it.
- Name the new type in [src/puter-js/index.d.ts](../src/puter-js/index.d.ts) if consumers should be able to import it. That file is the one hand-written declaration in the package: it decides what is public and re-exports nothing else.
### 5. Docs
- Add the method page at [src/docs/src/](../src/docs/src/)`<Area>/<method>.md` — frontmatter (`title`, `description`, `platforms`), syntax, parameters, return value, and at least one runnable example — and update the area overview (`<Area>.md`). Copy the structure of an existing page.
### 6. Tests
- **Backend:** colocated Vitest tests; prefer the in-memory test server (`setupPuterTestEnv` in [src/backend/testUtil.ts](../src/backend/testUtil.ts)) over mocking.
- **SDK:** add cases to [src/puter-js/tests/api/suites/](../src/puter-js/tests/api/suites/)`<area>.suite.ts` (register new suites in `suites/index.ts`). One suite runs on node, browser, and workerd via `npm run test:puterjs` — never write per-platform tests, and rebuild first with `npm run build:workerLib` since the runners execute the built bundle.
- **Desktop-rendered UI** (`puter.ui.*`): add a Playwright spec per [src/puter-js/TESTING.md](../src/puter-js/TESTING.md).
### 7. Security pass
Scan the diff before opening the PR: no internals leaked in errors or logs, no over-broad responses, auth gates present. Flag anything auth-, permission-, or data-export-related in the PR description.
## Maintaining an existing API
Changes are **additive by default**:
- New parameters are optional, with defaults that reproduce the old behavior exactly.
- Never rename, repurpose, or remove existing parameters, response fields, or error codes. Don't change types, ordering guarantees, or which fields are present when.
- New behavior that could surprise existing callers goes behind an opt-in flag.
- Docs, types, and tests move in the same PR as the behavior. A signature change with stale docs is a bug — the docs are the contract users code against.
- Bug fixes come with a regression test that fails before the fix. Be suspicious of fixes that change observable behavior: someone may depend on the bug. When in doubt, ask a maintainer.
### Breaking changes
Rare and deliberate. In order: explicit maintainer sign-off, a documented migration path, and a rollout plan — typically new surface added first, old surface deprecated, old surface removed much later, if ever. Never break as a side effect of a refactor.
### Deprecating
The old surface keeps working. Mark it `@deprecated` in the type declarations, note the replacement on its docs page, and stop using it in examples. Removal is a separate, maintainer-approved decision.
## Definition of done
- [ ] Backward compatible (or the break was explicitly approved)
- [ ] Right home: extension if core never calls it; layered structure either way
- [ ] puter.js method matches sibling conventions; errors are `{ message, code }` with stable codes
- [ ] Types updated and matching runtime behavior
- [ ] Docs page + area overview updated, with a runnable example
- [ ] Tests: backend + three-platform SDK suite (+ e2e for desktop-rendered UI)
<h3 align="center"><img width="80" alt="Puter.com، الحاسوب السحابي الشخصي: جميع ملفاتك وتطبيقاتك وألعابك في مكان واحد يمكن الوصول إليه من أي مكان في أي وقت." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">نظام تشغيل الإنترنت! مجاني ومفتوح المصدر وقابل للاستضافة الذاتية.</h3>
<p>بيوتر هو نظام تشغيل إنترنت متقدم ومفتوح المصدر، مصمم ليكون غنيًا بالميزات وسريعًا بشكل استثنائي وقابلًا للتوسع بدرجة كبيرة. يمكن استخدام بيوتر كـ:</p>
<ul>
<li>سحابة شخصية تعطي الأولوية للخصوصية لحفظ جميع ملفاتك وتطبيقاتك وألعابك في مكان آمن واحد، يمكن الوصول إليه من أي مكان وفي أي وقت.</li>
<li>منصة لبناء ونشر المواقع الإلكترونية وتطبيقات الويب والألعاب</li>
<li>بديل لـ Dropbox وGoogle Drive وOneDrive وغيرها، مع واجهة جديدة وميزات قوية.</li>
<li>بيئة سطح مكتب عن بُعد للخوادم ومحطات العمل.</li>
<li>مشروع ومجتمع ودود ومفتوح المصدر للتعلم عن تطوير الويب والحوسبة السحابية والأنظمة الموزعة والكثير غير ذلك!</li>
</ul>
</div>
<br/>
## البدء
### 💻 التطوير المحلي
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
سيؤدي هذا إلى تشغيل Puter على http://puter.localhost:4100 (أو المنفذ التالي المتاح).
نحن دائمًا سعداء لمساعدتك في أي أسئلة قد تكون لديك. لا تتردد في السؤال!
<br/>
## الترخيص
هذا المستودع، بما في ذلك جميع محتوياته ومشاريعه الفرعية ووحداته ومكوناته، مرخص تحت [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) ما لم ينص على خلاف ذلك صراحةً. قد تخضع المكتبات الخارجية المدرجة في هذا المستودع لتراخيصها الخاصة.
<h3 align="center"><img width="80" alt="Puter.com, ব্যক্তিগত ক্লাউড কম্পিউটার: আপনার সমস্ত ফাইল, অ্যাপস, এবং গেম এক জায়গায়, যেকোনো সময়, যেকোনো স্থান থেকে অ্যাক্সেসযোগ্য।" src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">ইন্টারনেট ওএস! ফ্রি, ওপেন-সোর্স, এবং সেল্ফ-হোস্টেবল।</h3>
Puter একটি উন্নত, ওপেন-সোর্স ইন্টারনেট অপারেটিং সিস্টেম যা বৈশিষ্ট্যপূর্ণ, অত্যন্ত দ্রুত এবং উচ্চ মাত্রায় সম্প্রসারণযোগ্য। Puter ব্যবহার করা যেতে পারে:
- একটি প্রাইভেসি-প্রথম পার্সোনাল ক্লাউড হিসাবে যা আপনার সমস্ত ফাইল, অ্যাপস এবং গেমসকে এক জায়গায় নিরাপদে রাখে, যেকোনো সময় যেকোনো স্থান থেকে অ্যাক্সেসযোগ্য।
- ওয়েবসাইট, ওয়েব অ্যাপ এবং গেম তৈরি ও প্রকাশ করার একটি প্ল্যাটফর্ম হিসাবে।
- ড্রপবক্স, গুগল ড্রাইভ, ওয়ানড্রাইভ ইত্যাদির বিকল্প হিসাবে একটি নতুন ইন্টারফেস এবং শক্তিশালী বৈশিষ্ট্য সহ।
- সার্ভার এবং ওয়ার্কস্টেশনের জন্য একটি রিমোট ডেস্কটপ এনভায়রনমেন্ট হিসাবে।
- ওয়েব ডেভেলপমেন্ট, ক্লাউড কম্পিউটিং, ডিস্ট্রিবিউটেড সিস্টেম এবং আরও অনেক কিছু শিখতে একটি বন্ধুত্বপূর্ণ, ওপেন-সোর্স প্রকল্প এবং কমিউনিটি হিসাবে!
<br/>
## শুরু করার জন্য
## 💻 লোকাল ডেভেলপমেন্ট
```bash
Copy code
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
এটি Puter কে http://puter.localhost:4100 (অথবা পরবর্তী উপলব্ধ পোর্টে) চালু করবে।
মেইনটেইনার এবং কমিউনিটির সাথে এই চ্যানেলগুলির মাধ্যমে সংযোগ করুন:
- বাগ রিপোর্ট বা ফিচার রিকোয়েস্ট? অনুগ্রহ করে একটি ইস্যু খুলুন।
- ডিসকর্ড: discord.com/invite/PQcx7Teh8u
- X (টুইটার): x.com/HeyPuter
- রেডিট: reddit.com/r/puter/
- মাস্টডন: mastodon.social/@puter
- সিকিউরিটি ইস্যু? security@puter.com
- মেইনটেইনারদের ইমেইল করুন hi@puter.com এ
আপনার যেকোনো প্রশ্নের জন্য আমরা সবসময় সাহায্য করতে প্রস্তুত। জিজ্ঞাসা করতে দ্বিধা করবেন না!
<br/>
## লাইসেন্স
এই রিপোজিটরি, এর সমস্ত বিষয়বস্তু, সাব-প্রকল্প, মডিউল, এবং কম্পোনেন্ট সহ [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) লাইসেন্সের অধীনে লাইসেন্সকৃত, যদি অন্যথায় স্পষ্টভাবে উল্লেখ না করা হয়। এই রিপোজিটরিতে অন্তর্ভুক্ত তৃতীয় পক্ষের লাইব্রেরিগুলি তাদের নিজস্ব লাইসেন্সের অধীনে হতে পারে।
<h3 align="center"><img width="80" alt="Puter.com, Den Personlige Cloudcomputer: Alle dine filer, apps og spil på ét sted tilgængelige fra hvor som helst til enhver tid." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">Internet OS'et! Gratis, Open-Source og kan selvhostes.</h3>
Puter er et avanceret, open-source internetoperativsystem designet til at være funktionsrigt, exceptionelt hurtigt og meget udvideligt. Puter kan bruges som:
- En privatlivsfokuseret personlig sky til at opbevare alle dine filer, apps og spil på ét sikkert sted, tilgængeligt hvor som helst og når som helst.
- En platform til at bygge og publicere hjemmesider, webapplikationer og spil.
- Et alternativ til Dropbox, Google Drive, OneDrive osv. med et friskt interface og kraftfulde funktioner.
- Et fjernskrivebordsmiljø for servere og arbejdsstationer.
- Et venligt, open-source projekt og fællesskab til at lære om webudvikling, cloud computing, distribuerede systemer og meget mere!
<br/>
## Kom godt i gang
### 💻 Lokal Udvikling
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
Dette vil starte Puter på http://puter.localhost:4100 (eller den næste tilgængelige port).
Vi er altid glade for at hjælpe dig med eventuelle spørgsmål, du måtte have. Tøv ikke med at spørge!
<br/>
## Licens
Dette repository, inklusive alt dets indhold, underprojekter, moduler og komponenter, er licenseret under [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt), medmindre andet er udtrykkeligt angivet. Tredjepartsbiblioteker inkluderet i dette repository kan være underlagt deres egne licenser.
<h3 align="center"><img width="80" alt="Puter.com, Der persönliche Cloud-Computer: Alle Ihre Dateien, Apps und Spiele an einem Ort, jederzeit und überall zugänglich." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">Das Internet-Betriebssystem! Kostenlos, Open-Source und selbst hostbar.</h3>
Puter ist ein fortschrittliches, Open-Source-Internet-Betriebssystem, das funktionsreich, außergewöhnlich schnell und hochgradig erweiterbar konzipiert wurde. Puter kann verwendet werden als:
- Eine datenschutzfreundliche persönliche Cloud, um alle Ihre Dateien, Apps und Spiele an einem sicheren Ort aufzubewahren, jederzeit und überall zugänglich.
- Eine Plattform zum Erstellen und Veröffentlichen von Websites, Webanwendungen und Spielen.
- Eine Alternative zu Dropbox, Google Drive, OneDrive usw. mit einer frischen Benutzeroberfläche und leistungsstarken Funktionen.
- Eine Remote-Desktop-Umgebung für Server und Workstations.
- Ein freundliches, Open-Source-Projekt und eine Community, um mehr über Webentwicklung, Cloud Computing, verteilte Systeme und vieles mehr zu lernen!
<br/>
## Erste Schritte
### 💻 Lokale Entwicklung
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
Dies startet Puter unter http://puter.localhost:4100 (oder dem nächsten verfügbaren Port).
Wir helfen Ihnen gerne bei allen Fragen, die Sie haben könnten. Zögern Sie nicht zu fragen!
<br/>
## Lizenz
Dieses Repository, einschließlich aller Inhalte, Unterprojekte, Module und Komponenten, ist unter [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) lizenziert, sofern nicht ausdrücklich anders angegeben. In diesem Repository enthaltene Bibliotheken von Drittanbietern können ihren eigenen Lizenzen unterliegen.
<h3 align="center"><img width="80" alt="Puter.com, The Personal Cloud Computer: All your files, apps, and games in one place accessible from anywhere at any time." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">The Internet OS! Free, Open-Source, and Self-Hostable.</h3>
Puter is an advanced, open-source internet operating system designed to be feature-rich, exceptionally fast, and highly extensible. Puter can be used as:
- A privacy-first personal cloud to keep all your files, apps, and games in one secure place, accessible from anywhere at any time.
- A platform for building and publishing websites, web apps, and games.
- An alternative to Dropbox, Google Drive, OneDrive, etc. with a fresh interface and powerful features.
- A remote desktop environment for servers and workstations.
- A friendly, open-source project and community to learn about web development, cloud computing, distributed systems, and much more!
<br/>
## Getting Started
### 💻 Local Development
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
This will launch Puter at http://puter.localhost:4100 (or the next available port).
We are always happy to help you with any questions you may have. Don't hesitate to ask!
<br/>
## License
This repository, including all its contents, sub-projects, modules, and components, is licensed under [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) unless explicitly stated otherwise. Third-party libraries included in this repository may be subject to their own licenses.
<h3 align="center"><img width="80" alt="Puter.com, El Computador Personal en Nube: Todos tus archivos, apps y juegos en un solo lugar accesible desde cualquier lugar en cualquier momento" src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">El Sistema Operativo de Internet! Gratis, de Código abierto, y Autohospedable.</h3>
<p align="center">
<a href="https://puter.com/"><strong>« DEMO EN VIVO »</strong></a>
Puter es un sistema operativo en internet avanzado y de código abierto, diseñado para ser rico en funcionalidades, excepcionalmente rápido y altamente extensible. Puter puede ser usado como:
- Una nube personal privada para almacenar todos tus archivos, aplicaciones y juegos en un lugar seguro, accesible y desde cualquier lugar en cualquier momento.
- Una plataforma para construir y publicar páginas web, aplicativos sobre la web y juegos.
- Una alternativa a Dropbox, Google Drive, OneDrive, etc. con una interfaz fresca y llena de funcionalidades.
- Un entorno de escritorio remoto para servidores y estaciones de trabajo.
- Un proyecto y comunidad abiertas y amigables para aprender sobre desarrollo web, computación en la nube, sistemas distribuidos y mucho más!
<br/>
## Primeros Pasos
### 💻 Desarrollo Local
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
✨ Esto ejecutará Puter en http://puter.localhost:4100 (o el siguiente puerto disponible).
Si esto no funciona, consulta [First Run Issues](./doc/self-hosters/first-run-issues.md) para obtener pasos de solución de problemas.
✨ Esto ejecutará Puter en http://puter.localhost:4100 (o el siguiente puerto disponible).
<br/>
### 🚀 Auto-Hospedaje
Para guías detalladas sobre cómo auto-hospedar Puter, incluyendo opciones de configuración y mejores prácticas, consulta nuestra [Documentación de Auto-Hospedaje](https://github.com/HeyPuter/puter/blob/main/doc/self-hosters/instructions.md).
### ☁️ Puter.com
Puter está disponible como servicio alojado en [**puter.com**](https://puter.com).
<br/>
## Requerimientos del sistema
- **Sistemas operativos:** Linux, macOS, Windows
- **RAM:** 2GB mínimo (4GB recomendados)
- **Almacenamiento:** 1GB de espacio libre
- **Node.js:** Versión 16+ (Versión 23+ recomendada)
- **npm:** Última version estable
<br/>
## Soporte
Conéctate con los mantenedores y la comunidad a través de estos canales:
- Reporte de bug o solicitud de funcionalidad? Por favor [abrir un issue](https://github.com/HeyPuter/puter/issues/new/choose).
Estamos siempre felices de ayudar con cualquier pregunta que puedas tener. No dudes en preguntar!
<br/>
## Licencia
Este repositorio, incluyendo todo su contenido, sub-proyectos, modulos y componentes, esta licenciado bajo [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) a menos que se indique explícitamente lo contrario. Librerías de terceros incluidos en este repositorio pueden estar sujetas a sus propias licencias.
<h3 align="center"><img width="80" alt="Puter.com، رایانش ابری شخصی: همه فایلها، برنامهها و بازیهای شما در یک مکان قابل دسترسی از هر جا و در هر زمان." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">سیستمعامل اینترنت! رایگان، متنباز، و قابل میزبانی شخصی.</h3>
<p>پیوتر یک سیستم عامل تحت وب پیشرفتهی متنباز است که به منظور ایجاد ویژگیهای متنوع، سرعت بسیار بالا، و مقیاسپذیری طراحی شده است. از پیوتر میتوان بهعنوان:</p>
<ul>
<li>یک فضای ابری شخصی که بر حریم خصوصی تمرکز دارد و تمام فایلها، برنامهها، و بازیهای شما را در یک مکان امن ذخیره میکند، قابل دسترسی از هر جا و در هر زمان.</li>
<li>پلتفرمی برای ساخت و انتشار وبسایتها، اپلیکیشنهای وب، و بازیها.</li>
<li>جایگزینی برای Dropbox، Google Drive، OneDrive، و سایر موارد، با یک رابط کاربری مدرن و قابلیتهای قدرتمند.</li>
<li>یک محیط دسکتاپ از راه دور برای سرورها و ایستگاههای کاری.</li>
<li> یک پروژه و جامعهی متنباز دوستانه برای یادگیری توسعه وب، رایانش ابری، سیستمهای توزیعشده، و موارد دیگر نام برد!</li>
</ul>
</div>
<br/>
## نحوهی استفاده
### 💻 توسعهی محلی
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
این کار پیوتر را در http://puter.localhost:4100 (یا پورت در دسترس بعدی) اجرا میکند.
ما همیشه از پاسخگویی به سوالات شما خرسند هستیم. در سوال پرسیدن درنگ نکنید!
## گواهی
این مخزن، شامل تمام محتویات، پروژههای فرعی، ماژولها و اجزای آن، تحت مجوز [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) است مگر آنکه خلاف آن بهطور صریح ذکر شده باشد. کتابخانههای خارجی ممکن است گواهیهای جداگانه داشته باشند.
<h3 align="center"><img width="80" alt="Puter.com, The Personal Cloud Computer: All your files, apps, and games in one place accessible from anywhere at any time." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">Internetin käyttöjärjestelmä! Ilmainen, avoimen lähdekoodin ja itse isännöitävä.</h3>
Puter on kehittynyt, avoimen lähdekoodin internetin käyttöjärjestelmä, joka on suunniteltu olemaan ominaisuuksiltaan rikas, poikkeuksellisen nopea ja erittäin laajennettava. Puteria voidaan käyttää:
- Yksityisyyttä kunnioittavana henkilökohtaisena pilvenä, johon voit tallentaa kaikki tiedostosi, sovelluksesi ja pelisi turvallisesti yhdessä paikassa, josta ne ovat saatavilla missä tahansa ja milloin tahansa.
- Alustana verkkosivustojen, web-sovellusten ja pelien rakentamiseen ja julkaisemiseen.
- Vaihtoehtona Dropboxille, Google Drivelle, OneDrivelle jne. tuoreella käyttöliittymällä ja tehokkailla ominaisuuksilla.
- Etätyöpöytäympäristönä palvelimille ja työasemille.
- Ystävällisenä, avoimen lähdekoodin projektina ja yhteisönä, jossa voit oppia verkkokehityksestä, pilvipalveluista, hajautetuista järjestelmistä ja paljon muusta!
<br/>
## Aloittaminen
### 💻 Paikallinen kehitys
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
Tämä käynnistää Puterin osoitteessa http://puter.localhost:4100 (tai seuraavassa vapaassa portissa).
Olemme aina valmiita auttamaan sinua kaikissa kysymyksissäsi. Älä epäröi kysyä!
<br/>
## Lisenssi
Tämä repository, mukaan lukien kaikki sen sisältö, aliprojektit, moduulit ja komponentit, on lisensoitu [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt)-lisenssillä, ellei toisin mainita. Tämän repositoryn mukana tulevat kolmannen osapuolen kirjastot voivat olla omien lisenssiensä alaisia.
<h3 align="center"><img width="80" alt="Puter.com, L'ordinateur cloud personnel : Tous vos fichiers, applications et jeux en un seul endroit accessible de partout à tout moment." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">L'OS Internet ! Gratuit, open-source et auto-hébergeable.</h3>
Puter est un système d'exploitation internet avancé, open-source, conçu pour être riche en fonctionnalités, extrêmement rapide et hautement extensible. Puter peut être utilisé comme :
- Un cloud personnel axé sur la confidentialité pour garder tous vos fichiers, applications et jeux en un seul endroit sécurisé, accessible de partout à tout moment.
- Une plateforme pour créer et publier des sites web, des applications web et des jeux.
- Une alternative à Dropbox, Google Drive, OneDrive, etc. avec une interface renouvelée et des fonctionnalités puissantes.
- Un environnement de bureau à distance pour serveurs et stations de travail.
- Un projet et une communauté open-source accueillants pour apprendre le développement web, l'informatique en nuage, les systèmes distribués, et bien plus encore !
<br/>
## Démarrage
### 💻 Développement Local
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
Cela lancera Puter à http://puter.localhost:4100 (ou au port disponible suivant).
Nous sommes toujours heureux de vous aider avec toutes les questions que vous pourriez avoir. N'hésitez pas à nous demander !
<br/>
## License
Ce dépôt, y compris tout son contenu, sous-projets, modules et composants, est licencié sous [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) sauf indication contraire explicite. Les bibliothèques tierces incluses dans ce dépôt peuvent être soumises à leurs propres licences.
אנחנו תמיד שמחים לעזור עם כל שאלה שיש. אל תהסס לשאול!
<br/>
## רישיון
ספריה זו, כולל כל התכנים שלה, תתי הפרויקטים, המודולים והרכיבים שלה, מורשית תחת [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) אלא אם נאמר אחרת במפורש. לספריות צד שלישי הכלולות בספרייה זו עשויות להיות רישיונות משלהן.
<h3 align="center"><img width="80" alt="Puter.com, The Personal Cloud Computer: आपकी सारी फाइलें, ऐप्स, और गेम एक ही जगह, जिसे कहीं से भी कभी भी एक्सेस किया जा सकता है।" src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">इंटरनेट ओएस! फ्री, ओपन-सोर्स, और सेल्फ-होस्टेबल।</h3>
Puter एक एडवांस्ड, ओपन-सोर्स इंटरनेट ऑपरेटिंग सिस्टम है जिसे फीचर-रिच, तेज़ और एक्सटेंडेबल बनाने के लिए डिज़ाइन किया गया है। Puter का उपयोग आप निम्नलिखित चीजों के लिए कर सकते हैं:
- एक प्राइवेसी-फर्स्ट पर्सनल क्लाउड, जो आपकी सभी फाइलों, ऐप्स और गेम्स को एक सेफ जगह पर रखता है, जिसे आप कहीं से भी कभी भी एक्सेस कर सकते हैं।
- वेबसाइट्स, वेब ऐप्स और गेम्स बनाने और पब्लिश करने का एक प्लेटफ़ॉर्म।
- Dropbox, Google Drive, OneDrive आदि का एक शानदार और पावरफुल इंटरफ़ेस वाला विकल्प।
- सर्वर और वर्कस्टेशन के लिए एक रिमोट डेस्कटॉप एनवायरनमेंट।
- एक फ्रेंडली ओपन-सोर्स प्रोजेक्ट और कम्युनिटी, जहां आप वेब डेवलपमेंट, क्लाउड कंप्यूटिंग, डिस्ट्रीब्यूटेड सिस्टम्स और बहुत कुछ सीख सकते हैं।
<br/>
## शुरुआत कैसे करें?
### 💻 लोकल डेवलपमेंट
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
✨ यह Puter को
<font color="red"> http://puter.localhost:4100 </font> (या अगले उपलब्ध पोर्ट) पर लॉन्च करेगा।
अगर यह काम नहीं करता, तो [First Run Issues](./doc/self-hosters/first-run-issues.md) देखें।
<font color="red"> http://puter.localhost:4100 (or the next available port). </font> (या अगले उपलब्ध पोर्ट) पर लॉन्च करेगा।
<br/>
### 🚀 सेल्फ-होस्टिंग
सेल्फ-होस्टिंग के लिए विस्तृत गाइड, कॉन्फ़िगरेशन ऑप्शन्स और बेस्ट प्रैक्टिसेज जानने के लिए हमारी [Self-Hosting Documentation](https://github.com/HeyPuter/puter/blob/main/doc/self-hosters/instructions.md) देखें।
<br/>
### ☁️ Puter.com
Puter [**puter.com**](https://puter.com) पर एक होस्टेड सर्विस के रूप में भी उपलब्ध है।
<br/>
## सिस्टम आवश्यकताएँ
* **ऑपरेटिंग सिस्टम्स:** Linux, macOS, Windows
* **RAM:** कम से कम 2GB (4GB रिकमेंडेड)
* **डिस्क स्पेस:** 1GB फ्री स्पेस
* **Node.js:** वर्जन 16+ (वर्जन 23+ रिकमेंडेड)
* **npm:** लेटेस्ट स्टेबल वर्जन
<br/>
## सपोर्ट
नीचे दिए गए माध्यमों से आप मेंटेनर्स और कम्युनिटी से जुड़ सकते हैं:
* बग रिपोर्ट या फीचर रिक्वेस्ट? [यहाँ issue खोलें](https://github.com/HeyPuter/puter/issues/new/choose)।
आपके किसी भी सवाल में मदद करने के लिए हम हमेशा तैयार हैं!
<br/>
## लाइसेंस
यह रिपॉज़िटरी और इसके सभी कंटेंट्स [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) लाइसेंस के अंतर्गत आते हैं जब तक कि कुछ और स्पष्ट रूप से ना लिखा हो। इसमें शामिल थर्ड-पार्टी लाइब्रेरीज़ अपने-अपने लाइसेंस के अधीन हो सकती हैं।
<br/>
## अनुवाद
Puter के डॉक्यूमेंटेशन कई भाषाओं में उपलब्ध हैं, जिनमें शामिल हैं:
<h3 align="center"><img width="80" alt="Puter.com, A személyi felhő számítógép: Minden fájl, alkalmazás és játék egy helyen elérhető bárhonnan, bármikor." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">Az internetes oprendszer! Ingyenes, nyílt-forráskódú, saját szerveren futtatható.</h3>
A Puter egy fejlett, nyílt forráskódú internetes operációs rendszer, amelyet úgy terveztek, hogy funkciókban gazdag, kivételesen gyors és nagymértékben bővíthető legyen. A Puter a következőképpen használható:
- Egy adatvédelmet előtérbe helyező személyes felhő, amely minden fájlt, alkalmazást és játékot egy biztonságos helyen tart. Bárhonnan és bármikor elérhető.
- Egy platform weboldalak, web-appok, és játékok készítéséhez/közzétételéhez.
- A Dropbox, Google Drive, OneDrive (stb.) alternatívája megújult felülettel és hatékony funkciókkal.
- Egy távoli desktop-környezet szervereknek és workstation-öknek.
- Egy barátságos, nyílt forráskódú projekt és közösség, amely a webfejlesztéssel, a felhőalapú számítástechnikával, elosztott rendszerekkel és sok más érdekes témával foglalkozik!
<br/>
## Első lépések
### 💻 Helyi (lokális) fejlesztés
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
Ezzel a http://puter.localhost:4100 -on futtatjuk Putert. (vagy a legközelebbi elérhető porton).
Mindig örömmel segítünk bármilyen felmerülő kérdésben. Bátran kérdezz tőlünk!
<br/>
## License
Ez a repo, beleértve annak minden tartalmát, alprojektjeit, moduljait és komponenseit, az [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) licenc alatt áll, hacsak másképp nem rendelkeznek róla. A repoban szereplő harmadik fél által fejlesztett könyvtárak saját licencfeltételek alá eshetnek.
<h3 align="center"><img width="80" alt="Puter.com, The Personal Cloud Computer: All your files, apps, and games in one place accessible from anywhere at any time." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">Ինտերնետ ՕՀ! Անվճար, բաց կոդով և ինքնահոսթ հնարավորությամբ։</h3>
Puter-ը առաջադեմ, բաց կոդով ինտերնետային օպերացիոն համակարգ է, որը նախագծված է լինել ֆունկցիոնալ հարուստ, բացառիկ արագ և բարձր ընդլայնելի։ Puter-ը կարող է օգտագործվել հետևյալ կերպ․
- Անձնական ամպային համակարգ՝ առաջնային գաղտնիությամբ, որը թույլ է տալիս պահել ձեր բոլոր ֆայլերը, հավելվածները և խաղերը մեկ անվտանգ վայրում, որը հասանելի է ցանկացած վայրից և ցանկացած ժամանակ։
- Պլատֆորմ կայքերի, վեբ հավելվածների և խաղերի ստեղծման և հրապարակման համար։
- Dropbox, Google Drive, OneDrive և այլ ծառայությունների այլընտրանք՝ նոր ինտերֆեյսով և հզոր գործառույթներով։
- Հեռավոր աշխատասեղանի միջավայր սերվերների և աշխատանքային կայանների համար։
- Պարզ, բաց կոդով նախագիծ և համայնք՝ վեբ ծրագրավորման, ամպային հաշվարկների, բաշխված համակարգերի և այլ թեմաների մասին սովորելու համար։
<br/>
## Սկսել
### 💻 Լոկալ ծրագրավորում
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
Սա կգործակի Puter-ը հետևյալ հասցեով՝ http://puter.localhost:4100 (կամ հաջորդ հասանելի պորտով)։
Մենք միշտ ուրախ ենք օգնել ձեզ ցանկացած հարցում։ Մի կաշկանդվեք հարցնել։
<br/>
## Լիցենզիա
Այս պահոցարանը, ներառյալ բոլոր իր բովանդակությունը, ենթա-պրոյեկտները, մոդուլները և բաղադրիչները, լիցենզավորվում են [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) լիցենզիայի տակ, եթե այլ կերպ հստակ նշված չէ։ Այս պահոցարանում ներառված երրորդ կողմի գրադարանները կարող են ենթարկվել իրենց սեփական լիցենզիաներին։
<h3 align="center"><img width="80" alt="Puter.com, Komputer Cloud Pribadi: Semua file, aplikasi, dan permainan Anda berada di satu tempat yang dapat diakses dari mana saja kapan saja." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">Sistem Operasi Internet! Gratis, Sumber Terbuka, dan Dapat Dihosting Sendiri.</h3>
Puter adalah sistem operasi internet canggih, open-source, yang dirancang untuk menjadi kaya fitur, sangat cepat, dan sangat dapat diperluas. Puter dapat digunakan sebagai:
- Cloud pribadi yang mengutamakan privasi untuk menyimpan semua file, aplikasi, dan permainan Anda di satu tempat yang aman, yang dapat diakses dari mana saja kapan saja.
- Platform untuk membangun dan mempublikasikan situs web, aplikasi web, dan permainan.
- Alternatif untuk Dropbox, Google Drive, OneDrive, dll. Dengan antarmuka baru dan fitur-fitur canggih.
- Lingkungan desktop jarak jauh untuk server dan workstation.
- Proyek dan komunitas open-source yang ramah untuk belajar tentang pengembangan web, komputasi gemawan (cloud), sistem terdistribusi, dan banyak lagi!
<br/>
## Memulai
### 💻 Pengembangan Lokal
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
Ini akan menjalankan Puter di http://puter.localhost:4100 (atau di port berikutnya yang tersedia)
Kami selalu senang membantu Anda dengan pertanyaan apa pun yang Anda miliki. Jangan ragu untuk bertanya!
<br/>
## Lisensi
Repositori ini, termasuk semua isinya, sub-proyek, modul, dan komponen, dilisensikan di bawah [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) kecuali dinyatakan sebaliknya secara eksplisit. Perpustakaan pihak ketiga yang termasuk dalam repositori ini mungkin tunduk pada lisensinya sendiri.
<h3 align="center"><img width="80" alt="Puter.com, The Personal Cloud Computer: All your files, apps, and games in one place accessible from anywhere at any time." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">Il sistema operativo di Internet! Gratuito, Open-Source e Auto-Hostabile.</h3>
Puter è un sistema operativo di Internet avanzato e open-source, progettato per essere ricco di funzionalità, eccezionalmente veloce e altamente estensibile. Puter può essere utilizzato come:
- Un cloud personale che tiene conto della privacy per conservare tutti i file, le app e i giochi in un luogo sicuro, accessibile da qualsiasi luogo e in qualsiasi momento.
- Una piattaforma per creare e pubblicare siti web, app e giochi.
- Un'alternativa a Dropbox, Google Drive, OneDrive, ecc. con un'interfaccia nuova e funzioni potenti.
- Un ambiente desktop remoto per server e workstation.
- Un progetto e una comunità open-source amichevole per imparare lo sviluppo web, il cloud computing, i sistemi distribuiti e molto altro ancora!
<br/>
## Getting Started
### 💻 Local Development
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
In questo modo Puter verrà avviato all'indirizzo http://puter.localhost:4100 (o alla prossima porta disponibile).
Siamo sempre felici di aiutarvi con qualsiasi domanda. Non esitate a chiedere!
<br/>
## Licenza
Questo repository, compresi tutti i suoi contenuti, sottoprogetti, moduli e componenti, è concesso in licenza [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt), a meno che non sia esplicitamente indicato diversamente. Le librerie di terze parti incluse in questo repository possono essere soggette alle loro licenze.
<h3 align="center"><img width="80" alt="Puter.com, The Personal Cloud Computer: All your files, apps, and games in one place accessible from anywhere at any time." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">Puter: 인터넷 OS! 무료이고 오픈소스이며 자체 호스팅이 가능합니다.</h3>
이 저장소는 모든 내용, 하위 프로젝트, 모듈 및 구성 요소를 포함하여 명시적으로 달리 명시되지 않는 한 [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) 라이선스 하에 제공됩니다. 이 저장소에 포함된 제3자 라이브러리는 해당 라이브러리의 고유 라이선스를 따를 수 있습니다.
<h3 align="center"><img width="80" alt="Puter.com, The Personal Cloud Computer: All your files, apps, and games in one place accessible from anywhere at any time." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">ഇന്റർനെറ്റ് ഓപ്പറേറ്റിംഗ് സിസ്റ്റം!<br> സൗജന്യവും, ഓപ്പൺ സോഴ്സും സ്വയം ഹോസ്റ്റ് ചെയ്യാൻ പറ്റുന്നതും</h3>
ഫീച്ചറുകളാൽ സമ്പുഷ്ടവും അസാധാരണമാംവിധം വേഗതയേറിയതും,വളരെ വിപുലീകരിക്കാവുന്നതുമായ ഒരു നൂതന, ഓപ്പൺ സോഴ്സ് ഇന്റർനെറ്റ് ഓപ്പറേറ്റിംഗ് സിസ്റ്റമാണ് പ്യൂട്ടർ. പ്യൂട്ടർ ഇനിപ്പറയുന്ന രീതിയിൽ ഉപയോഗിക്കാം:
- നിങ്ങളുടെ എല്ലാ ഫയലുകളും ആപ്പുകളും ഗെയിമുകളും ഒരു സുരക്ഷിത സ്ഥലത്ത് സൂക്ഷിക്കുന്നതിനുള്ള, സ്വകാര്യതയ്ക്ക് മുൻഗണന കൊടുക്കുന്ന ആദ്യത്തെ വ്യക്തിഗത ക്ലൗഡ്, എവിടെ നിന്നും എപ്പോൾ വേണമെങ്കിലും ആക്സസ് ചെയ്യാൻ കഴിയും.
- വെബ്സൈറ്റുകൾ, വെബ് ആപ്പുകൾ, ഗെയിമുകൾ എന്നിവ നിർമ്മിക്കുന്നതിനും പ്രസിദ്ധീകരിക്കുന്നതിനുമുള്ള ഒരു പ്ലാറ്റ്ഫോം.
- പുതിയ ഇന്റർഫേസും, ശക്തമായ ഫീച്ചറുകളും അടങ്ങിയ, ഡ്രോപ്പ്ബോക്സ്, ഗൂഗിൾ ഡ്രൈവ്, വൺഡ്രൈവ് മുതലായവയ്ക്കുള്ള ബദൽ.
- സെർവറുകൾക്കും വർക്ക്സ്റ്റേഷനുകൾക്കും, ഒരു വിദൂര ഡെസ്ക്ടോപ്പ് പരിസ്ഥിതി.
- വെബ് ഡെവലപ്മെന്റ്, ക്ലൗഡ് കംപ്യൂട്ടിംഗ്, ഡിസ്ട്രിബ്യൂട്ടഡ് സിസ്റ്റങ്ങൾ എന്നിവയെ കുറിച്ചും, അതിലേറെ കാര്യങ്ങളെ കുറിച്ചും അറിയാനുള്ള സൗഹൃദപരവും ഓപ്പൺ സോഴ്സുമായ പ്രോജക്റ്റും കമ്മ്യൂണിറ്റിയും!
<br/>
## തുടങ്ങാനായി
### 💻 ലോക്കൽ ഡെവലപ്മെന്റ്
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
ഇത് http://puter.localhost:4100 (അല്ലെങ്കിൽ അടുത്ത ലഭ്യമായ പോർട്ടിൽ) എന്നതിൽ Puter സമാരംഭിക്കും
നിങ്ങൾക്ക് ഉണ്ടായേക്കാവുന്ന ഏത് ചോദ്യങ്ങളിലും നിങ്ങളെ സഹായിക്കുന്നതിൽ ഞങ്ങൾക്ക് എപ്പോഴും സന്തോഷമുണ്ട്. ചോദിക്കാൻ മടിക്കേണ്ട!
<br/>
## ലൈസൻസ്
ഈ ശേഖരം, അതിന്റെ എല്ലാ ഉള്ളടക്കങ്ങളും, ഉപപദ്ധതികളും, മൊഡ്യൂളുകളും, ഘടകങ്ങളും ഉൾപ്പെടെ, [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) എന്നതിന് കീഴിൽ ലൈസൻസുള്ളതാണ്. ഈ ശേഖരത്തിൽ ഉൾപ്പെടുത്തിയിരിക്കുന്ന മൂന്നാം കക്ഷി ലൈബ്രറികൾ അവരുടെ സ്വന്തം ലൈസൻസുകൾക്ക് വിധേയമായിരിക്കാം.
<h3 align="center"><img width="80" alt="Puter.com, The Personal Cloud Computer: Semua fail, apl, dan permainan anda di satu tempat yang boleh diakses dari mana sahaja pada bila-bila masa." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">Sistem Operasi Internet! Percuma, Sumber Terbuka, dan Boleh Dihoskan Sendiri.</h3>
Puter ialah sistem operasi internet sumber terbuka yang maju dan direka untuk kaya dengan ciri kefungsian, kepantasan luar biasa dan kebolehluasan yang tinggi. Puter boleh digunakan sebagai:
- Storan awan peribadi yang mendahulukan privasi untuk menyimpan semua fail, aplikasi dan permainan anda di satu tempat yang selamat dan boleh diakses dari mana sahaja pada bila-bila masa.
- Platform untuk membina dan menerbitkan laman web, aplikasi web dan permainan.
- Alternatif kepada Dropbox, Google Drive, OneDrive, dan lain-lain dengan antara muka yang baharu dan ciri kefungsian berkuasa tinggi.
- Persekitaran desktop awan untuk server dan stesen kerja.
- Projek dan komuniti sumber terbuka yang mesra untuk mempelajari pembangunan laman web, pengkomputeran awan, sistem teragih, dan banyak lagi!
<br/>
## Mulakan
### 💻 Pembangunan Lokal
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
Ini akan melancarkan Puter di http://puter.localhost:4100 (atau port seterusnya yang tersedia).
Kami sentiasa gembira untuk membantu anda dengan apa-apa soalan. Jangan takut untuk bertanya!
<br/>
## Lesen
Repositori ini, termasuklah kandungannya, subprojek, modul dan komponen, dilesenkan di bawah [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) melainkan dinyatakan sebaliknya. *Library* pihak ketiga yang terkandung dalam repositori ini tertakluk kepada lesen mereka sendiri.
<!-- The word `Library` is kept as is to avoid confusion since the direct translation `Perpustakaan/Pustaka` is never used in the tech context and doesn't convey the same meaning among Malay community if used in this situation -->
<h3 align="center"><img width="80" alt="Puter.com, De Persoonlijke Cloud Computer: Al je bestanden, apps en games op één plek, overal en altijd toegankelijk." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">Het Internet OS! Gratis, Open-Source en Zelf te Hosten.</h3>
Puter is een geavanceerd, open-source internetbesturingssysteem ontworpen om functierijk, uitzonderlijk snel en zeer uitbreidbaar te zijn. Puter kan worden gebruikt als:
- Een privacy-gerichte persoonlijke cloud om al je bestanden, apps en games op één veilige plek te bewaren, overal en altijd toegankelijk.
- Een platform voor het bouwen en publiceren van websites, web-apps en games.
- Een alternatief voor Dropbox, Google Drive, OneDrive, etc. met een frisse interface en krachtige functies.
- Een externe desktopomgeving voor servers en werkstations.
- Een vriendelijk, open-source project en gemeenschap om te leren over webontwikkeling, cloud computing, gedistribueerde systemen en veel meer!
<br/>
## Aan de slag
### 💻 Lokale Ontwikkeling
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
Dit zal Puter starten op http://puter.localhost:4100 (of de eerstvolgende beschikbare poort).
We helpen je graag met al je vragen. Aarzel niet om te vragen!
<br/>
## Licentie
Deze repository, inclusief alle inhoud, subprojecten, modules en componenten, is gelicentieerd onder [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) tenzij expliciet anders vermeld. Bibliotheken van derden die in deze repository zijn opgenomen, kunnen onderworpen zijn aan hun eigen licenties.
<h3 align="center"><img width="80" alt="Puter.com, The Personal Cloud Computer: All your files, apps, and games in one place accessible from anywhere at any time." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center"><img width="80" alt="Puter.com, The Personal Cloud Computer: All your files, apps, and games in one place accessible from anywhere at any time." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center"><img width="80" alt="Puter.com, Osobisty Komputer Chmurowy: Wszystkie twoje pliki, aplikacje i gry w jednym miejscu, dostępne z dowolnego miejsca o dowolnej porze." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center"> System Operacyjny Internet! Darmowy, Open-Source i Możliwy do Samodzielnego Hostowania.</h3>
Puter to zaawansowany, open-source'owy internetowy system operacyjny, zaprojektowany tak, aby był bogaty w funkcje, wyjątkowo szybki i wysoce rozszerzalny. Puter może być używany jako:
- Prywatna chmura osobista do przechowywania wszystkich plików, aplikacji i gier w jednym bezpiecznym miejscu, dostępnym z dowolnego miejsca o dowolnej porze.
- Platforma do budowania i publikowania stron internetowych, aplikacji webowych i gier.
- Alternatywa dla Dropbox, Google Drive, OneDrive itp. ze świeżym interfejsem i potężnymi funkcjami.
- Zdalne środowisko pulpitu dla serwerów i stacji roboczych.
- Przyjazny, open-source'owy projekt i społeczność do nauki o tworzeniu stron internetowych, chmurze obliczeniowej, systemach rozproszonych i wielu innych!
<br/>
## Rozpoczęcie pracy
## 💻 Lokalne środowisko developerskie
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
To uruchomi Puter na http://puter.localhost:4100 (lub na następnym dostępnym porcie).
Puter jest dostępny jako usługa hostowana na [**puter.com**](https://puter.com).
<br/>
## Wymagania systemowe
- **Systemy operacyjne:** Linux, macOS, Windows
- **RAM:** Minimum 2GB (zalecane 4GB)
- **Przestrzeń dyskowa:** 1GB wolnego miejsca
- **Node.js:** Wersja 16+ (zalecana wersja 22+)
- **npm:** Najnowsza stabilna wersja
<br/>
## Wsparcie
Skontaktuj się z opiekunami i społecznością przez te kanały:
- Raport o błędzie lub prośba o funkcję? Proszę otworzyć zgłoszenie.
- Discord: discord.com/invite/PQcx7Teh8u
- X (Twitter): x.com/HeyPuter
- Reddit: reddit.com/r/puter/
- Mastodon: mastodon.social/@puter
- Problemy z bezpieczeństwem? security@puter.com
- Email do opiekunów: hi@puter.com
Zawsze chętnie pomożemy Ci z wszelkimi pytaniami, jakie możesz mieć. Nie wahaj się pytać!
<br/>
## Licencja
To repozytorium, w tym cała jego zawartość, podprojekty, moduły i komponenty, jest licencjonowane na podstawie [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt), chyba że wyraźnie zaznaczono inaczej. Biblioteki stron trzecich zawarte w tym repozytorium mogą podlegać własnym licencjom.
<h3 align="center"><img width="80" alt="Puter.com, O Computador Pessoal em Nuvem: Todos os seus arquivos, aplicativos e jogos em um único lugar, acessíveis de qualquer lugar e a qualquer hora." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">O Sistema Operacional da Internet! Gratuito, de Código Aberto e Auto-Hospedável.</h3>
<p align="center">
<a href="https://puter.com/"><strong>« DEMONSTRAÇÃO AO VIVO »</strong></a>
Puter é um sistema operacional de internet avançado e de código aberto, projetado para ser rico em recursos, excepcionalmente rápido e altamente extensível. Puter pode ser usado como:
- Um serviço de nuvem pessoal com foco na privacidade para manter todos os seus arquivos, aplicativos e jogos em um local seguro, acessível de qualquer lugar e a qualquer hora.
- Uma plataforma para construir e publicar websites, aplicativos web e jogos.
- Uma alternativa ao Dropbox, Google Drive, OneDrive, etc., com uma interface renovada e recursos poderosos.
- Um ambiente de desktop remoto para servidores e estações de trabalho.
- Um projeto e comunidade de código aberto e amigável para aprender sobre desenvolvimento web, computação em nuvem, sistemas distribuídos e muito mais!
<br/>
## Iniciando o Projeto
### 💻 Desenvolvimento Local
```
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
✨ Isso iniciará o Puter em http://puter.localhost:4100 (ou na próxima porta disponível).
Se isso não funcionar, consulte [First Run Issues](./doc/self-hosters/first-run-issues.md) para solucionar os problemas.
✨ Isso iniciará o Puter em http://puter.localhost:4100 (ou na próxima porta disponível).
<br/>
### 🚀 Auto-Hospedagem
Para guia detalhados sobre como auto-hospedar o Puter, incluindo opções de configuração e melhores práticas, consulte nossa [Documentação de Auto-Hospedagem](https://github.com/HeyPuter/puter/blob/main/doc/self-hosters/instructions.md).
### ☁️ Puter.com
O Puter está disponível como um serviço hospedado em [**puter.com**](https://puter.com).
<br/>
## Requerimentos do sistema
- **Sistema operacional:** Linux, macOS, Windows
- **RAM:** 2GB mínimo (4GB recomendado)
- **Espaço de disco:** 1GB de espaço disponível
- **Node.js:** Versão 16+ (Versão 23+ recomendada)
- **npm:** Última versão estável
<br/>
## Suporte
Conecte-se com os mantenedores e a comunidade através destes canais:
- Relato de bug ou solicitação de recurso? Por favor, [abra um tópico](https://github.com/HeyPuter/puter/issues/new/choose).
Estamos sempre felizes em ajudá-lo com quaisquer perguntas que você possa ter. Não hesite em perguntar!
<br/>
## Licença
Este repositório, incluindo todos os seus conteúdos, subprojetos, módulos e componentes, está licenciado sob [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) a menos que explicitamente indicado de outra forma. Bibliotecas de terceiros incluídas neste repositório podem estar sujeitas às suas próprias licenças.
<h3 align="center"><img width="80" alt="Puter.com, calculatorul personal în cloud: toate fișierele, aplicațiile și jocurile tale într-un singur loc, accesibile de oriunde și oricând." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">Sistemul de operare al internetului! Gratuit, open-source și găzduibil autonom.</h3>
<p align="center">
<img alt="Dimensiunea repoului GitHub" src="https://img.shields.io/github/repo-size/HeyPuter/puter"> <img alt="Versiunea de pe GitHub" src="https://img.shields.io/github/v/release/HeyPuter/puter?label=ultima%20versiune"> <img alt="Licență GitHub" src="https://img.shields.io/github/license/HeyPuter/puter">
</p>
<p align="center">
<a href="https://puter.com/"><strong>« DEMO LIVE »</strong></a>
<a href="https://hackerone.com/puter_h1b">Program de recompense pentru identificarea bugurilor</a>
</p>
<h3 align="center"><img width="800" style="border-radius:5px;" alt="captură de ecran" src="https://assets.puter.site/puter.com-screenshot-3.webp"></h3>
<br/>
## Puter
Puter este un sistem de operare pe internet, avansat, open-source, conceput să fie bogat în funcționalități, excepțional de rapid și foarte extensibil. Puter poate fi folosit ca:
* Un cloud personal cu accent pe confidențialitate, pentru a-ți păstra toate fișierele, aplicațiile și jocurile într-un singur loc securizat, accesibil de oriunde și oricând.
* O platformă pentru a construi și publica site-uri, aplicații web și jocuri.
* O alternativă la Dropbox, Google Drive, OneDrive etc., cu o interfață nouă și funcționalități puternice.
* Un mediu desktop la distanță pentru servere și stații de lucru.
* Un proiect și o comunitate, open-source și prietenoase, pentru a învăța despre dezvoltare web, cloud computing, sisteme distribuite și multe altele!
<br/>
## Fă primii pași
### 💻 Dezvoltare locală
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
Aceasta va porni Puter la [http://puter.localhost:4100](http://puter.localhost:4100) (sau pe următorul port disponibil).
Suntem întotdeauna bucuroși să te ajutăm cu orice întrebări ai. Nu ezita să ne pui întrebări!
<br/>
## Licență
Acest repository, inclusiv tot conținutul său, subproiectele, modulele și componentele, este licențiat sub [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt), cu excepția cazurilor în care se menționează explicit altfel. Bibliotecile terțe incluse în acest repository pot fi supuse propriilor lor licențe.
<h3 align="center"><img width="80" alt="Puter.com, персональный облачный компьютер: все ваши файлы, приложения и игры в одном месте, доступные из любой точки мира в любое время." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">Интернет ОС! Бесплатная, с открытым исходным кодом и возможностью самостоятельной установки.</h3>
Puter — это передовая операционная система с открытым исходным кодом, разработанная для обеспечения широкого функционала, исключительной скорости и высокой масштабируемости. Puter можно использовать как:
- Персональное облако с приоритетом конфиденциальности для хранения всех ваших файлов, приложений и игр в одном безопасном месте, доступном из любой точки мира в любое время.
- Платформа для создания и публикации веб-сайтов, веб-приложений и игр.
- Альтернатива Dropbox, Google Drive, OneDrive и т. д. с новым интерфейсом и мощными функциями.
- Удаленное рабочее окружение для серверов и рабочих станций.
- Дружественный проект с открытым исходным кодом и сообщество для изучения веб-разработки, облачных вычислений, распределенных систем и многого другого!
<br/>
## Начало работы
### 💻 Локальная разработка
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
Это запустит Puter по адресу http://puter.localhost:4100 (или на следующем доступном порту).
Мы всегда рады помочь вам с любыми вопросами. Не стесняйтесь спрашивать!
<br/>
## Лицензия
Этот репозиторий, включая все его содержимое, подпроекты, модули и компоненты, лицензирован в соответствии с [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt), если явно не указано иное. Сторонние библиотеки, включенные в этот репозиторий, могут подпадать под действие их собственных лицензий.
<h3 align="center"><img width="80" alt="Puter.com, Den personliga molndatorn: Alla dina filer, appar och spel på ett ställe tillgängliga var som helst när som helst." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">Internet OS! Gratis, öppen källkod och självhostad.</h3>
Puter är ett avancerat, öppen källkod internetoperativsystem designat för att vara funktionsrikt, exceptionellt snabbt och mycket utbyggbart. Puter kan användas som:
- Ett integritetsfokuserat personligt moln för att hålla alla dina filer, appar och spel på ett säkert ställe, tillgängligt var som helst när som helst.
- En plattform för att bygga och publicera webbplatser, webbappar och spel.
- Ett alternativ till Dropbox, Google Drive, OneDrive, etc. med ett fräscht gränssnitt och kraftfulla funktioner.
- En fjärrskrivbordsmiljö för servrar och arbetsstationer.
- Ett vänligt, öppen källkod-projekt och gemenskap för att lära sig om webbutveckling, molndatorer, distribuerade system och mycket mer!
<br/>
## Komma igång
### 💻 Lokal Utveckling
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
Detta kommer att starta Puter på http://puter.localhost:4100 (eller nästa lediga port).
Vi hjälper dig gärna med eventuella frågor du kan ha. Tveka inte att fråga!
<br/>
## Licens
Detta arkiv, inklusive allt dess innehåll, delprojekt, moduler och komponenter, är licensierat under [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) om inte annat uttryckligen anges. Tredjepartsbibliotek som ingår i detta arkiv kan vara föremål för sina egna licenser.
<h3 align="center"><img width="80" alt="Puter.com, The Personal Cloud Computer: உங்கள் கோப்புகள், ஆப்ஸ் மற்றும் கேம்கள் அனைத்தும் ஒரே இடத்தில் எங்கிருந்தும் எந்த நேரத்திலும் அணுகலாம்." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">இன்டர்நெட் OS! இலவசம், ஓப்பன் சோர்ஸ் மற்றும் Self-Hostable</h3>
புட்டர்(putter) என்பது ஒரு மேம்பட்ட, திறந்த மூல இலவசமாக இணைய இயக்க முறைமையாகும், இது அம்சம் நிறைந்ததாகவும், விதிவிலக்காக வேகமாகவும், அதிக விரிவாக்கக்கூடியதாகவும் வடிவமைக்கப்பட்டுள்ளது. புட்டரை இவ்வாறு பயன்படுத்தலாம்:
- உங்கள் கோப்புகள், பயன்பாடுகள் மற்றும் கேம்கள் அனைத்தையும் ஒரே பாதுகாப்பான இடத்தில் வைத்திருக்க, எந்த நேரத்திலும் எங்கிருந்தும் அணுகக்கூடிய தனியுரிமை-முதல் தனிப்பட்ட கிளவுட்.
- இணையதளங்கள், இணைய பயன்பாடுகள் மற்றும் கேம்களை உருவாக்கி வெளியிடுவதற்கான தளம் இதுவாகும்.
- புதிய இடைமுகம் மற்றும் சக்திவாய்ந்த அம்சங்களுடன் Dropbox, Google Drive, OneDrive போன்றவற்றுக்கு மாற்றீடாக உபயோகிக்க கூடியது.
- சர்வர்கள் மற்றும் பணிநிலையங்களுக்கான தொலைநிலை டெஸ்க்டாப்(desktop) சூழல்.
- வலை மேம்பாடு, கிளவுட் கம்ப்யூட்டிங், விநியோகிக்கப்பட்ட அமைப்புகள் மற்றும் பலவற்றைப் பற்றி அறிந்து ஒரு நட்பு ரீதியான, திறந்த மூல திட்டம் மற்றும் சமூக அறிவியலில் சார்ந்த ஒன்று.
<br/>
## தொடங்குதல்
### 💻 உள்ளூர் வளர்ச்சி
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
``` தொடக்கம்
```
இது புட்டரை <http://puter.localhost:4100> இல் தொடங்கும் (அல்லது அடுத்து கிடைக்கும் இடம்).
உங்களுக்கு ஏதேனும் கேள்விகள் இருந்தால் உங்களுக்கு உதவ நாங்கள் எப்போதும் மகிழ்ச்சியடைகிறோம். தயங்காமல் கேளுங்கள்!
<br/>
## உரிமம்
இந்தக் களஞ்சியமானது, அதன் அனைத்து உள்ளடக்கங்கள், துணைத் திட்டங்கள், தொகுதிகள் மற்றும் கூறுகள் உட்பட, வெளிப்படையாகக் கூறப்படாவிட்டால், [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) இன் கீழ் உரிமம் பெற்றுள்ளது. . இந்தக் களஞ்சியத்தில் சேர்க்கப்பட்டுள்ள மூன்றாம் தரப்பு நூலகங்கள் அவற்றின் சொந்த உரிமங்களுக்கு உட்பட்டதாக இருக்கும்.
<h3 align="center"><img width="80" alt="Puter.com, The Personal Cloud Computer: మీ అన్ని ఫైల్లు, యాప్లు మరియు గేమ్లను ఒకే స్థలంలో ఎక్కడి నుండైనా ఎప్పుడైనా యాక్సెస్ చేయవచ్చు." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center"> ఇంటర్నెట్ OS! ఉచిత, ఓపెన్ సోర్స్, and Self-Hostable.</h3>
పుటర్ అనేది అధునాతన, ఓపెన్ సోర్స్ ఇంటర్నెట్ ఆపరేటింగ్ సిస్టమ్, ఇది ఫీచర్-రిచ్, అనూహ్యంగా వేగవంతమైన మరియు అత్యంత విస్తరించదగినదిగా రూపొందించబడింది. పుటర్ను ఇలా ఉపయోగించవచ్చు:
- మీ అన్ని ఫైల్లు, యాప్లు మరియు గేమ్లను ఒకే సురక్షిత స్థలంలో ఉంచడానికి గోప్యత-మొదటి వ్యక్తిగత క్లౌడ్, ఎప్పుడైనా ఎక్కడి నుండైనా యాక్సెస్ చేయవచ్చు.
- వెబ్సైట్లు, వెబ్ యాప్లు మరియు గేమ్లను రూపొందించడానికి మరియు ప్రచురించడానికి ఒక వేదిక.
- తాజా ఇంటర్ఫేస్ మరియు శక్తివంతమైన ఫీచర్లతో Dropbox, Google Drive, OneDrive మొదలైన వాటికి ప్రత్యామ్నాయం.
- సర్వర్లు మరియు వర్క్స్టేషన్ల కోసం రిమోట్ డెస్క్టాప్ వాతావరణం.
- వెబ్ డెవలప్మెంట్, క్లౌడ్ కంప్యూటింగ్, డిస్ట్రిబ్యూట్ సిస్టమ్లు మరియు మరిన్నింటి గురించి తెలుసుకోవడానికి స్నేహపూర్వక, ఓపెన్ సోర్స్ ప్రాజెక్ట్ మరియు కమ్యూనిటీ!
<br/>
## ప్రారంభించడం
### లోకల్ డెవలప్మెంట్
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
ఇది http://puter.localhost:4100 (లేదా తదుపరి అందుబాటులో ఉన్న పోర్ట్) వద్ద పుటర్ని ప్రారంభిస్తుంది.
ఇది పని చేయకపోతే, దీని కోసం [మొదటి రన్ సమస్యలు](./doc/first-run-issues.md) చూడండి
మీకు ఏవైనా సందేహాలు ఉంటే మీకు సహాయం చేయడానికి మేము ఎల్లప్పుడూ సంతోషిస్తాము. అడగడానికి సంకోచించకండి!
<br/>
## లైసెన్సు
ఈ రిపోజిటరీ, దాని మొత్తం కంటెంట్లు, ఉప-ప్రాజెక్ట్లు, మాడ్యూల్స్ మరియు కాంపోనెంట్లతో సహా, [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) కింద లైసెన్స్ని కలిగి ఉంటుంది. . ఈ రిపోజిటరీలో చేర్చబడిన థర్డ్-పార్టీ లైబ్రరీలు వాటి స్వంత లైసెన్స్లకు లోబడి ఉండవచ్చు.
<h3 align="center"><img width="80" alt="Puter.com, The Personal Cloud Computer: All your files, apps, and games in one place accessible from anywhere at any time." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center"><img width="80" alt="Puter.com, Kişisel Bulut Bilgisayar: Tüm dosyalarınız, uygulamalarınız ve oyunlarınız her zaman her yerden erişilebilen tek bir yerde." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">İnternet İşletim Sistemi! Ücretsiz, Açık Kaynaklı ve Kendi Kendine Barındırılabilir</h3>
Puter, zengin özelliklere sahip, son derece hızlı ve son derece genişletilebilir olacak şekilde tasarlanmış gelişmiş, açık kaynaklı bir internet işletim sistemidir. Puter şu şekilde kullanılabilir:
- Tüm dosyalarınızı, uygulamalarınızı ve oyunlarınızı tek bir güvenli yerde tutmak için gizlilik öncelikli bir kişisel bulut, her yerden her zaman erişilebilir.
- Web siteleri, web uygulamaları ve oyunlar oluşturmak ve yayınlamak için bir platform.
- Yeni bir arayüz ve güçlü özelliklerle Dropbox, Google Drive, OneDrive vb. uygulamalara bir alternatif.
- Sunucular ve iş istasyonları için bir uzak masaüstü ortamı.
- Web geliştirme, bulut bilişim, dağıtık sistemler ve çok daha fazlası hakkında bilgi edinmek için dost canlısı, açık kaynaklı bir proje ve topluluk!
<br/>
## Başlarken
### 💻 Yerel Geliştirme
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
Bu, Puter'ı http://puter.localhost:4100 adresinde (veya bir sonraki kullanılabilir portta) başlatacaktır.
Sorularınız varsa size her zaman yardımcı olmaktan mutluluk duyarız. Sormaktan çekinmeyin!
<br/>
## Lisans
Bu depo, tüm içeriği, alt projeleri, modülleri ve bileşenleri dahil olmak üzere, aksi açıkça belirtilmedikçe [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) altında lisanslanmıştır. Bu depoda yer alan üçüncü taraf kütüphaneler kendi lisanslarına tabi olabilir.
<h3 align="center"><img width="80" alt="Puter.com, The Personal Cloud Computer: Всі ваші файли, додатки та ігри в одному місці, доступні з будь-якого куточка світу в будь-який час." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">Інтернет ОС! Безкоштовна, відкрита та self-hosted.</h3>
<p align="center">
<img alt="Розмір репозиторію на GitHub" src="https://img.shields.io/github/repo-size/HeyPuter/puter"> <img alt="Остання версія на GitHub" src="https://img.shields.io/github/v/release/HeyPuter/puter?label=latest%20version"> <img alt="Ліцензія GitHub" src="https://img.shields.io/github/license/HeyPuter/puter">
Puter — це просунута, інтернет-ОС, з відкритим кодом, створена для того, щоб бути багатофункціональною, надзвичайно швидкою та розширюваною. Puter може використовуватися як:
- Приватний хмарний сервіс для збереження всіх ваших файлів, додатків і ігор у безпечному місці, доступному в будь-який час з будь-якого місця.
- Платформа для створення та публікації вебсайтів, вебдодатків і ігор.
- Альтернатива Dropbox, Google Drive, OneDrive і тд, з свіженьким інтерфейсом та потужними функціями.
- Віддалене робоче середовище для серверів і робочих станцій.
- Дружній, відкритий проєкт та спільнота для вивчення веброзробки, хмарних обчислень, розподілених систем і багато іншого!
<br/>
## Початок роботи
### Локальна розробка
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
Це запустить Puter на http://puter.localhost:4100 (або на наступному доступному порті).
Ми завжди готові допомогти Вам з будь-якими питаннями, що можуть виникнути. Не соромтеся ставити нам питання!
<br/>
## License
Цей репорзиторій, включаючи увесь його контент, дочірні проєкти, модулі, і компоненти, ліцензовано за [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt), якщо явно не вказано інше. Сторонні бібліотеки, включені в цей репозиторій, можуть підпадати під дію інших ліцензій.
<h3 align="center"><img width="80" alt="Puter.com, ذاتی کلاؤڈ کمپیوٹر: آپ کی تمام فائلیں، ایپس، اور کھیل ایک جگہ پر، کہیں سے بھی اور کسی بھی وقت قابل رسائی۔" src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">انٹرنیٹ OS! مفت، اوپن سورس، اور خود میزبان.</h3>
ایک جدید، اوپن سورس انٹرنیٹ آپریٹنگ سسٹم ہے جو کہ خصوصیات سے بھرپور، بہت تیز، اور انتہائی توسیع پذیر ہے۔ Puter
: کو استعمال کیا جا سکتا ہے Puter
- ایک پرائیویسی فرسٹ ذاتی کلاؤڈ کے طور پر تاکہ آپ کی تمام فائلیں، ایپس، اور کھیل ایک محفوظ جگہ پر رکھی جا سکیں، کہیں سے بھی اور کسی بھی وقت قابل رسائی ہوں۔
- ویب سائٹس، ویب ایپس، اور کھیل بنانے اور شائع کرنے کے لئے ایک پلیٹ فارم کے طور پر۔
- وغیرہ کا متبادل، نئے انٹرفیس اور طاقتور خصوصیات کے ساتھ۔ Dropbox، Google Drive، OneDrive
- سرورز اور ورک اسٹیشنز کے لیے ایک ریموٹ ڈیسک ٹاپ ماحول کے طور پر۔
- ویب ڈویلپمنٹ، کلاؤڈ کمپیوٹنگ، تقسیم شدہ نظاموں، اور بہت کچھ سیکھنے کے لیے ایک دوستانہ، اوپن سورس پروجیکٹ اور کمیونٹی!
<br/>
## شروع کرنے کا طریقہ
### 💻 مقامی ترقی
```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
یہ Puter کو http://puter.localhost:4100 (یا اگلے دستیاب پورٹ) پر لانچ کرے گا۔
ہم ہمیشہ آپ کی مدد کے لیے خوش ہیں۔ سوالات پوچھنے میں ہچکچاہٹ نہ کریں
!
<br/>
## لائسنس
اس ریپوزٹری، بشمول اس کے تمام مواد، ذیلی پروجیکٹس، ماڈیولز، اور کمپوننٹس، کو [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt) کے تحت لائسنس کیا گیا ہے جب تک کہ واضح طور پر کہیں اور نہ کہا گیا ہو۔ اس ریپوزٹری میں شامل تھرڈ پارٹی لائبریریاں اپنی لائسنس کے تابع ہو سکتی ہیں۔
<h3 align="center"><img width="80" alt="Puter.com, Máy Tính Đám Mây Cá Nhân: Tất cả các tệp, ứng dụng, và trò chơi của bạn ở một nơi, có thể truy cập từ bất cứ đâu vào bất kỳ lúc nào." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">Hệ điều hành Internet! Miễn phí, Mã nguồn mở và Có thể tự lưu trữ.</h3>
<p align="center">
<img alt="Kích thước repo GitHub" src="https://img.shields.io/github/repo-size/HeyPuter/puter"> <img alt="Phiên bản phát hành GitHub" src="https://img.shields.io/github/v/release/HeyPuter/puter?label=Phi%C3%AAn%20b%E1%BA%A3n%20ph%C3%A1t%20h%C3%A0nh%20GitHub"> <img alt="Giấy phép GitHub" src="https://img.shields.io/github/license/HeyPuter/puter">
</p>
<p align="center">
<a href="https://puter.com/"><strong>« DEMO TRỰC TIẾP »</strong></a>
Puter là một hệ điều hành internet tiên tiến, mã nguồn mở được thiết kế để có nhiều tính năng, tốc độ vượt trội và khả năng mở rộng cao. Puter có thể được sử dụng như:
- Một đám mây cá nhân ưu tiên quyền riêng tư để lưu trữ tất cả các tệp, ứng dụng và trò chơi của bạn ở một nơi an toàn, có thể truy cập từ bất cứ đâu, bất cứ lúc nào.
- Một nền tảng để xây dựng và xuất bản các trang web, ứng dụng web và trò chơi.
- Một sự thay thế cho Dropbox, Google Drive, OneDrive, v.v. với giao diện mới mẻ và nhiều tính năng mạnh mẽ.
- Một môi trường máy tính từ xa cho các máy chủ và máy trạm.
- Một dự án thân thiện, mã nguồn mở và cộng đồng để học hỏi về phát triển web, điện toán đám mây, hệ thống phân tán và nhiều hơn nữa!
<br/>
## Bắt Đầu
## 💻 Phát Triển Cục Bộ
```bash
Copy code
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start
```
Điều này sẽ khởi chạy Puter tại http://puter.localhost:4100 (hoặc cổng kế tiếp có sẵn).
Puter có sẵn dưới dạng dịch vụ lưu trữ tại [**puter.com**](https://puter.com).
<br/>
## Yêu Cầu Hệ Thống
- **Hệ Điều Hành:** Linux, macOS, Windows
- **RAM:** Tối thiểu 2GB (Khuyến nghị 4GB)
- **Dung Lượng Ổ Cứng:** Còn trống 1GB
- **Node.js:** Phiên bản 16+ (Khuyến nghị phiên bản 22+)
- **npm:** Phiên bản ổn định mới nhất
<br/>
## Hỗ Trợ
Kết nối với các nhà bảo trì và cộng đồng thông qua các kênh sau:
- Báo cáo lỗi hoặc yêu cầu tính năng? Vui lòng mở một vấn đề.
- Discord: discord.com/invite/PQcx7Teh8u
- X (Twitter): x.com/HeyPuter
- Reddit: reddit.com/r/puter/
- Mastodon: mastodon.social/@puter
- Vấn đề bảo mật? security@puter.com
- Email các nhà bảo trì tại hi@puter.com
Chúng tôi luôn sẵn sàng giúp đỡ bạn với bất kỳ câu hỏi nào bạn có. Đừng ngần ngại hỏi!
<br/>
## Giấy Phép
Kho lưu trữ này, bao gồm tất cả nội dung, dự án con, mô-đun và thành phần của nó, được cấp phép theo [AGPL-3.0](https://github.com/HeyPuter/puter/blob/main/LICENSE.txt), trừ khi được tuyên bố rõ ràng khác. Các thư viện của bên thứ ba được bao gồm trong kho lưu trữ này có thể phải tuân theo các giấy phép riêng của chúng.
<metaname="description"content="Puter is a privacy-first personal cloud to keep all your files, apps, and games in one private and secure place, accessible from anywhere at any time.">
<metaproperty="og:description"content="Puter is a privacy-first personal cloud to keep all your files, apps, and games in one private and secure place, accessible from anywhere at any time.">
<metaname="twitter:description"content="Puter is a privacy-first personal cloud to keep all your files, apps, and games in one private and secure place, accessible from anywhere at any time.">
The GUI is a single page application (SPA) and as best practice any route under root (`/*`) should preferably load the `index.html` file. However, there are situations where we want to load a custom page for a specific route: for example, the `/privacy` route may need to load a page that contains your privacy policy and has nothing to do with the GUI application. In these cases it is ok to load a custom page as long as the following essential GUI routes are loaded with the GUI (i.e. `index.html` file):
-`/app/*`
-`/action/*`
In other words, consider the routes above as "reserved" for Puter.
### Publish My Website
Right-click anywhere on the desktop to display options
From the options menu, select "New".
Then, choose "Folder".
Give the folder a name according to your preference.
After creating the folder:
Right-click on the folder.
Select the option "Publish as Website".
### Best Practices
- The `title` tags and meta tags (`<title></title>`, `<meta property="og:title"`, `<meta name="twitter:title"`, ...) should be dynamically set by the server. For example, if the URL is of an app (e.g. `https://puter.com/app/editor`) the `title` tags and meta tags should contain the app's title rather than the generic Puter title.
- The `description` meta tags (`<meta name="description"`, `<meta property="og:description"`, `<meta name="twitter:description"`, ...) should be dynamically set by the server. For example, if the URL is of an app (e.g. `https://puter.com/app/editor`) the `description` meta tags should contain the app's description rather than the generic Puter description.
- Make sure to escape any HTML code that is dynamically added to the HTML page. For example, if the app's description is `Puter is a <b>privacy-first</b> personal cloud to keep all your files, apps, and games in one private and secure place, accessible from anywhere at any time.` the `<b>` tag should be escaped to `<b>` so that the browser doesn't interpret it as an HTML tag.
- Make sure to replace all new line characters with space when dynamically adding text to the HTML page.
- Generally, for UX and SEO reasons make sure that the tags are filled with relevant information about the state the URL is representing. E.g. is the user on the desktop or an app?
Ensure the hosting device has a static IP address to prevent potential connectivity issues due to IP changes. This setup will enable seamless access to Puter and its services across your local network.
### Using Hosts Files
The hosts file is a straightforward way to map domain names to IP addresses on individual devices. It's simple to set up but requires manual changes on each device that needs access to the domains.
#### Windows
1. Open Notepad as an administrator.
2. Open the file located at `C:\Windows\System32\drivers\etc\hosts`.
3. Add lines for your domain and subdomain with the server's IP address, in the
following format:
```
192.168.1.10 puter.local
192.168.1.10 api.puter.local
```
#### For macOS and Linux:
1. Open a terminal.
2. Edit the hosts file with a text editor, e.g., `sudo nano /etc/hosts`.
3. Add lines for your domain and subdomain with the server's IP address, in the
following format:
```
192.168.1.10 puter.local
192.168.1.10 api.puter.local
```
4. Save and exit the editor.
### Using Router Configuration
Some routers allow you to add custom DNS rules, letting you configure domain names network-wide without touching each device.
1. Access your router’s admin interface (usually through a web browser).
2. Look for DNS or DHCP settings.
3. Add custom DNS mappings for `puter.local` and `api.puter.local` to the hosting device's IP address.
4. Save the changes and reboot the router if necessary.
This method's availability and steps may vary depending on your router's model and firmware.
### Using Local DNS
Setting up a local DNS server on your network allows for flexible and scalable domain name resolution. This method works across all devices automatically once they're configured to use the DNS server.
#### Options for DNS Software:
- **Pi-hole**: Acts as both an ad-blocker and a DNS server. Ideal for easy setup and maintenance.
- **BIND9**: Offers comprehensive DNS server capabilities for complex setups.
- **dnsmasq**: Lightweight and suitable for smaller networks or those new to running a DNS server.
**contributors note:** feel free to add any software you're aware of
which might help with this to the list. Also, feel free to add instructions here for specific software; our goal is for Puter to be easy to setup with tools you're already familiar with.
#### General Steps:
1. Choose and install DNS server software on a device within your network.
2. Configure the DNS server to resolve `puter.local` and `api.puter.local` to the IP address of your Puter hosting device.
3. Update your router's DHCP settings to distribute the DNS server's IP address to all devices on the network.
By setting up a local DNS server, you gain the most flexibility and control over your network's domain name resolution, ensuring that all devices can access Puter and its API without manual configuration.
## Production Configuration
Please note the self-hosting feature is still in alpha and a public production
deployment is not recommended at this time. However, if you wish to host
publicly you can do so following the same steps you normally would to configure
a domain name and ensuring the `api` subdomain points to the server as well.
> The self-hosted version of Puter is currently in alpha stage and should not be used in production yet. It is under active development and may contain bugs, other issues. Please exercise caution and use it for testing and evaluation purposes only.
### Self-Hosting Differences
Currently, the self-hosted version of Puter is different in a few ways from [Puter.com](https://puter.com):
- There is no built-in way to install or create other apps (see below)
- Several "core" apps are missing, such as **Code** or **Draw**, because we can't include them in this repository
- Some icons are different
Work is ongoing to improve the **App Center** and make it available on self-hosted.
Until then, it's possible to add other apps by manually editing the database file.
This process is not recommended unless you know what you are doing.
The file will appear after you first launch Puter, and should be found in `puter/data/puter-database.sqlite` for Docker,
or `volatile/runtime/puter-database.sqlite` otherwise.
You will need a database tool that can understand SQLite databases.
<br/>
## Configuration
Running the server will generate a configuration file in one of these locations:
-`config/config.json` when [Using Docker](#using-docker)
-`volatile/config/config.json` in [Local Development](#local-development)
-`/etc/puter/config.json` on a server (or within a Docker container)
### Domain Name
To access Puter on your device, you can simply go to the address printed in
the server console (usually `puter.localhost:4100`).
To access Puter from another device, a domain name must be configured, as well as
an `api` subdomain. For example, `example.local` might be the domain name pointing
to the IP address of the server running puter, and `api.example.com` must point to
this address as well. This domain must be specified in the configuration file
(usually `volatile/config/config.json`) as well.
See [domain configuration](./domains.md) for more information.
### Configure the Port
- You can specify a custom port by setting `http_port` to a desired value
- If you're using a reverse-proxy such as nginx or cloudflare, you should
also set `pub_port` to the public (external) port (usually `443`)
- If you have HTTPS enabled on your reverse-proxy, ensure that
`protocol` in config.json is set accordingly
### Default User
By default, Puter will create a user called `default_user`.
This user will have a randomly generated password, which will be printed
in the development console.
A warning will persist in the dev console until this user's
password is changed. Please login to this user and change the password as
`docker-compose.yml` brings up Puter **plus every external service it needs** — MariaDB, Valkey, DynamoDB-local, RustFS S3, Caddy — wired together. Closest thing to a production deployment you can self-manage on a single host.
## One-line installer (recommended)
```bash
curl -fsSL https://raw.githubusercontent.com/HeyPuter/puter/main/install.sh | sh
```
Generates secrets, writes `.env` + `puter/config/config.json`, downloads `docker-compose.yml` + `caddy/Caddyfile` from the OSS repo, and runs `docker compose up -d`. Re-running is safe — it won't overwrite existing config (set `PUTER_FORCE=1` to rotate). Use the manual steps below if you want to inspect or tweak each step yourself.
## Requirements
- **Docker** with the `compose` plugin.
- A **domain** with DNS access — you need a wildcard record (`*.your-domain.com` → server IP). Puter routes by subdomain (`api.<domain>`, `site.<domain>`, `app.<domain>`).
- Optional: **TLS certs** (or `certbot` to grab them — see Step 3).
| `puter-ollama-init` | `ollama/ollama` | One-shot — pulls the default model (`tinyllama`) on first boot |
State lives under `./puter/data/<service>/`.
---
## Step 1 — Create `.env` and `puter/config/config.json`
> ⚠️ **Run this whole block in one shell session.** It generates secrets once and writes them into both `.env` (read by docker compose) and `config.json` (read by Puter). The two files **must** agree on the MariaDB password and the S3 secret — if they drift, MariaDB initialises with one password and Puter tries to log in with another, and you get `ER_ACCESS_DENIED_ERROR`.
```bash
MARIADB_ROOT_PASSWORD=$(openssl rand -hex 32)
MARIADB_PASSWORD=$(openssl rand -hex 32)
S3_SECRET_KEY=$(openssl rand -hex 32)
JWT_SECRET_V2=$(openssl rand -hex 64)
URL_SIGNATURE_SECRET=$(openssl rand -hex 64)
cat > .env <<EOF
HTTP_PORT=80
# HTTPS_PORT=443 # uncomment after enabling TLS in Step 3
Replace `puter.localhost`, `site.puter.localhost`, `host.puter.localhost`, `dev.puter.localhost` and `app.puter.localhost` with your actual domain (or leave it for a localhost-only trial).
Why these knobs:
-`jwt_secret_v2` — the HMAC secret Puter signs and verifies auth tokens with (`kid: 'v2'` JWT header). The pre-v2 token format is retired: a token signed with the old `jwt_secret` no longer verifies, and holders are asked to sign in again. If you are upgrading from a release that had `jwt_secret`, drop it from your config — it is ignored.
-`env: "prod"` — the bundled `config.default.json` ships with `env: "dev"` (matches the source-tree `npm run start:gui` workflow, which expects webpack-dev-server emitting a CSS manifest). Self-host runs against pre-built static bundles, so `env: "prod"` makes the homepage emit the `/dist/bundle.min.css``<link>` tag instead of waiting on a manifest that doesn't exist.
-`database.migrationPaths` — Puter applies the bundled MySQL/MariaDB schema on boot. The migration files are idempotent, so it is safe to leave this configured across restarts.
-`dynamo.bootstrapTables: true` — Puter creates its KV table on boot. **Only set against a local emulator**, never real AWS.
-`dynamo.aws` keys are dummies; DynamoDB-local doesn't validate them but the AWS SDK requires _something_. **Note:** DynamoDB uses `access_key` / `secret_key` (snake_case); S3 below uses `accessKeyId` / `secretAccessKey` (camelCase). Not interchangeable.
-`providers.ollama.enabled: false` — Puter auto-probes a local Ollama at `127.0.0.1:11434` by default; without one running you'd see `ECONNREFUSED` on every boot. To run a bundled Ollama, see [Optional: local LLM (Ollama)](#optional-local-llm-ollama) below.
-`s3.s3Config.forcePathStyle: true` — RustFS / MinIO / fauxqs need path-style URLs (`<endpoint>/<bucket>`). Real AWS S3 wants virtual-hosted (`<bucket>.<endpoint>`) — drop this flag (or set `false`) when you swap to real S3.
-`s3.s3Config.publicEndpoint` — `endpoint` (`http://s3:9000`) only resolves inside the docker network; presigned upload/download URLs handed to the browser need a host-reachable URL. Caddy routes the `s3.<domain>` subdomain to RustFS internally and preserves the Host header end-to-end (required for S3 signature validation), so the browser hits the same port/protocol as the rest of the app — no separate published port, no mixed-content surprises when you turn on TLS. Switch to `https://s3.<your-domain>` once you enable TLS in Step 3. Real AWS S3 doesn't need this — its endpoint is already public; drop the field entirely.
-`trust_proxy: 1` — Caddy terminates TLS and forwards `X-Forwarded-For`. Without this, `req.ip` is the docker-network address of the Caddy container instead of the real client IP, which breaks rate limiting and IP-based audit logs. `1` = one trusted hop (Caddy). Bump to `2` if you put Cloudflare in front of Caddy; never set `true` (it trusts every hop and makes XFF forgeable).
> If you ever change `MARIADB_PASSWORD` after first boot, `.env` alone won't update MariaDB — its credentials are baked into `./puter/data/mariadb/` on first init. Either rotate the password inside MariaDB by hand or `docker compose down && rm -rf ./puter/data/mariadb` to start fresh.
## Step 2 — Point DNS at the server \[Optional\]
In your DNS provider, add records for the main domain plus the subdomains Puter and Caddy route on (`api.*`, `site.*`, `app.*`, `s3.*`):
```
A puter.localhost → <your server's public IP>
A *.puter.localhost → <your server's public IP>
A site.puter.localhost → <your server's public IP>
A *.site.puter.localhost → <your server's public IP>
A host.puter.localhost → <your server's public IP>
A *.host.puter.localhost → <your server's public IP>
A app.puter.localhost → <your server's public IP>
A *.app.puter.localhost → <your server's public IP>
A dev.puter.localhost → <your server's public IP>
A *.dev.puter.localhost → <your server's public IP>
```
The wildcards are required — Puter routes via subdomains (`api.*`, `app.*`, etc.) and Caddy routes browser S3 traffic via `s3.*` to RustFS.
## Step 3 — TLS (recommended for public installs) \[Optional\]
Skip this for a quick local demo. Don't skip it for users typing passwords.
**Get a wildcard cert.** Easiest path with Let's Encrypt + DNS-01 (works for wildcards):
```bash
sudo certbot certonly --manual --preferred-challenges dns \
The cert needs to cover `*.puter.localhost` so that `s3.puter.localhost` (browser S3 endpoint), plus Puter's own `api.*` / `app.*` subdomains, all validate.
> **Why not Caddy's automatic HTTPS?** Caddy issues certs over ACME by itself, but only for hostnames it knows up front, and only over HTTP-01 with the stock image. Puter serves every user's site and app on a subdomain it invents at runtime (`<name>.site.<domain>`, `<name>.app.<domain>`), which needs a **wildcard** cert — and wildcards require DNS-01, which requires a DNS-provider plugin that isn't in `caddy:2.11-alpine`. So `caddy/Caddyfile` sets `auto_https off` and reads the cert you supply below. If you'd rather have Caddy manage certs, build an image with the plugin for your DNS provider (`xcaddy build --with github.com/caddy-dns/<provider>`) and swap the `tls` line for a `tls { dns <provider> … }` block.
Drop the resulting `fullchain.pem` and `privkey.pem` into `./puter/tls/`.
**Wire Caddy to use them:**
1. Open [caddy/Caddyfile](../caddy/Caddyfile) and uncomment the `# :443 { … }` block at the bottom.
2. (Optional but recommended) Replace the plain `:80 { import puter_routes }` block with the `redir` version shown alongside it, to force HTTPS everywhere.
3. In [docker-compose.yml](../docker-compose.yml), uncomment the `443:443` port mapping under the `caddy` service.
The bundled `puter-caddy` mirrors production and is the supported default — it terminates TLS (Step 3) and forwards every Host to Puter. But plenty of self-hosters already run their own edge proxy (Traefik, nginx, HAProxy, a cloud load balancer, another Caddy). You can put Puter behind it instead; Puter doesn't terminate TLS itself in any setup, so "your proxy does TLS" is just a matter of pointing it at the Puter container and getting a handful of details right.
You can keep the bundled Caddy as the single hop your proxy talks to, or bypass it and forward straight to the Puter container on port `4100` (uncomment the `4100:4100` mapping under the `puter` service in [docker-compose.yml](../docker-compose.yml), or attach your proxy to the compose network). Either works — what matters is the rules below.
**The rules that actually matter** (getting any of these wrong is what causes the redirect loops and "Invalid Host header" failures people hit):
1. **Don't rewrite the `Host` header.** Puter routes entirely on Host — `api.<domain>`, `site.<domain>`, `app.<domain>` are all distinguished by the incoming host. Forward the original host through unchanged. Do **not** rewrite external hostnames to an internal name like `puter.local`; that forces you to remap every subdomain by hand and still breaks signed-URL and CORS checks. Most proxies preserve Host by default — just don't override it.
2. **The external domain must equal `domain` in your `config.json`.** If users reach the box at `puter.example.com`, then `domain` must be `puter.example.com` and the hosting domains must be its real subdomains (`site.puter.example.com`, `app.puter.example.com`, …) — exactly as the installer/`config.json` lays them out. Puter rejects hosts it doesn't recognize with `Invalid Host header`.
3. **Set `protocol` to match the public scheme.** When your proxy terminates TLS, set `"protocol": "https"` (installer: `PUTER_PROTOCOL=https`). Puter builds its origins, redirects, signed S3 URLs, and OIDC callback URLs from this — leave it `http` behind an HTTPS proxy and you get redirect loops, broken logins, and mixed-content errors.
4. **Forward the standard proxy headers.** Puter needs:
- `Host` — the original request host (rule 1).
- `X-Forwarded-Proto` — the **external** scheme (`https`), so Puter knows TLS was terminated upstream.
- `X-Forwarded-For` — the real client IP (rate limiting + audit logs).
- `Upgrade` / `Connection` — passed through for WebSocket / socket.io upgrades, or the realtime connection silently fails.
5. **Set `trust_proxy` to the number of hops in front of Puter.** One external proxy talking directly to Puter → `"trust_proxy": 1` (installer: `PUTER_TRUST_PROXY=1`). A second proxy in front (e.g. Cloudflare → your proxy → Puter) → `2`. This is the count of proxies between the client and Puter, **including** the bundled Caddy if you keep it. Too low and `req.ip` becomes a proxy address (breaks rate limiting); never set `true` (it trusts every hop and makes `X-Forwarded-For` forgeable).
6. **Route the wildcard to Puter.** Your proxy must forward `*.<domain>` (covering `api.*`, `app.*`, and `s3.<domain>`) and `*.site.<domain>` (and any other hosting domains) to Puter — same wildcard DNS as Step 2. Puter does the per-subdomain routing internally; the proxy just needs to hand it the traffic with the Host intact.
With those in place the rest of the stack is unchanged — `docker compose up -d` as below.
## Step 4 — Bring it up
Both [docker-compose.yml](../docker-compose.yml) and [caddy/Caddyfile](../caddy/Caddyfile) need to sit next to your `.env` — clone the repo, or copy those two paths out of it. The Caddyfile is bind-mounted read-only; if it's missing, Docker creates a *directory* at that path and `puter-caddy` dies with "not a directory".
```bash
docker compose up -d
```
First boot takes ~30s while MariaDB initialises and Puter applies the schema + default apps. Watch:
```bash
docker compose logs -f puter
```
Healthy startup:
```
[config] override from /etc/puter/config.json
[mysql] running migrations from /opt/puter/dist/src/backend/clients/database/migrations/mysql: 2 file(s)
[mysql] applied mysql_mig_1.sql (...)
[mysql] applied mysql_mig_2.sql (9 statements)
```
Then open **<https://puter.local>** (or `http://` if you skipped TLS). Login is `admin` — the temp password is printed once in the puter container logs on first boot:
```bash
docker compose logs puter | grep tmp_password
```
Change it in Settings after first login.
## Additional configuration
All optional. Drop any of the blocks below into `puter/config/config.json` and `docker compose restart puter`. See [config.template.jsonc](../config.template.jsonc) for the full list. Per-key documentation lives in [src/backend/types.ts](../src/backend/types.ts).
### PostgreSQL database
> **Community contribution — expect rough edges.** PostgreSQL support was contributed by the community and is not run by Puter.com production. It boots, applies the bundled schema, and exercises the common user/app/fsentry/session/permission/OIDC flows in the integration tests, but less-traveled SQL call sites may still need porting. If you hit a query that doesn't work on Postgres, please open an issue. For production self-hosting today, MariaDB/MySQL and SQLite are the supported defaults.
The bundled Docker Compose stack still defaults to MariaDB. To use PostgreSQL instead, run a PostgreSQL service yourself, point Puter at it, and use the PostgreSQL migration path:
You may use `"connectionString": "postgres://puter:...@postgres:5432/puter"` instead of the host/user/password fields. Puter only needs normal PostgreSQL connection details and the migration path; CloudNativePG is one possible Kubernetes operator, but no operator-specific manifests are required by Puter.
### Email (SMTP)
Used for password resets, email confirmation, and notifications. Without it those flows silently fail.
To require email confirmation before login, also set `"strict_email_verification_required": true`.
### Sign in with Google (or another OIDC provider)
```json
"oidc": {
"providers": {
"google": {
"client_id": "...apps.googleusercontent.com",
"client_secret": "...",
"scopes": "openid email profile"
}
}
}
```
Add `https://puter.<your-domain>/auth/oidc/callback/login` to the OAuth client's authorized redirect URIs in the Google Cloud Console. For non-Google providers, replace `google` with a custom id and supply `authorization_endpoint` / `token_endpoint` / `userinfo_endpoint` explicitly.
The `client_id` is your Apple Services ID. `team_id`, `key_id`, and `private_key` come from the Apple Developer Portal (Keys section — create a key with "Sign in with Apple" enabled). The `private_key` is the contents of the `.p8` file Apple provides. Add `https://puter.<your-domain>/auth/oidc/callback/login` and `https://puter.<your-domain>/auth/oidc/callback/signup` as return URLs in the Apple Services ID configuration.
### Sign in with Microsoft
```json
"oidc": {
"providers": {
"microsoft": {
"client_id": "YOUR_APPLICATION_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET_VALUE",
"tenant_id": "YOUR_TENANT_ID"
}
}
}
```
Register an app in the Azure Portal (Microsoft Entra ID → App registrations). The `client_id` is the Application (client) ID, `client_secret` is a client secret value, and `tenant_id` is the Directory (tenant) ID. Use `common` as `tenant_id` to allow any Microsoft account (personal and organizational); omit it to default to `common`. Add `https://puter.<your-domain>/auth/oidc/callback/login` and `https://puter.<your-domain>/auth/oidc/callback/signup` as redirect URIs under Authentication in the app registration.
### AI providers
Any provider with a key set is auto-enabled. Same shape as `ollama` above:
```json
"providers": {
"claude": { "apiKey": "sk-ant-..." },
"openai-completion": { "apiKey": "sk-..." },
"gemini": { "apiKey": "..." },
"openai-image-generation": { "apiKey": "sk-..." }
}
```
Full provider list (chat, image, video, TTS, OCR) is in the template.
### Per-user storage quota
Default is 100 MB per user.
```json
"storage_capacity": 5368709120, // 5 GB
"is_storage_limited": true
```
Set `is_storage_limited: false` for unlimited (bounded by host disk).
### Usage metering and budgets
Puter meters what an account costs to serve — bytes sent back, object-store
requests, KV capacity, AI tokens — against a monthly budget, and refuses the
operations that spend it once that budget is gone. The refusal is a `402` with
code `insufficient_funds`; reads that only describe things, and every kind of
deletion, stay available so an account can always see what it has and clear it.
On a self-hosted install this is almost certainly not what you want. There is
nowhere to buy more, so accounts are held to the free monthly allowance
(US$0.25 of measured cost — roughly 2 GiB of downloads) and start being refused
after that. Turn it off:
```json
"unlimitedMetering": true
```
Every account then resolves to an unlimited policy. Usage is still recorded, so
the dashboard still shows what is being consumed; nothing is ever refused for
lack of budget.
To keep the budgets but stop them blocking anything — recording only:
```json
"meteringEnforcement": { "enabled": false }
```
Calls driven by a deployed worker are exempt from enforcement by default,
because a worker has no prompt to show and nobody watching it fail. Set
`"meteringEnforcement": { "workers": true }` to include them.
### Captcha on signup / login
Built-in proof-of-work captcha — no external service needed.
The `ollama` and `ollama-init` services live behind a compose profile so they don't run unless you ask for them. By default, `puter/config/config.json` has `"ollama": { "enabled": false }` — Puter skips the auto-probe entirely. To run a local model:
`ollama-init` exits 0 once the model is pulled. Subsequent boots find the model already on disk and the pull is a fast no-op.
Without `--profile ai`, the `ollama` containers stay down and Puter (with `enabled: false`) doesn't try to reach them — the rest of the stack runs identically.
For GPU acceleration (NVIDIA), uncomment the `deploy:` block under the `ollama` service in [docker-compose.yml](../docker-compose.yml). Requires `nvidia-container-toolkit` on the host.
## Building from source instead of pulling
If you want to test local Dockerfile changes against the full stack, uncomment the `build:` block in [docker-compose.yml](../docker-compose.yml) under the `puter` service, change `pull_policy: always` → `pull_policy: never`, then:
```bash
docker compose up -d --build
```
---
## Managing running backend
```bash
# update
docker compose pull
docker compose up -d
# logs
docker compose logs -f puter
# stop, keep data
docker compose down
# stop, NUKE all state (irreversible)
docker compose down
rm -rf puter/data
```
Migrations re-apply idempotently across pulls. Volumes are preserved.
## Troubleshooting
**Site loads but I get a 502 / "Bad Gateway" from Caddy.**
The puter container failed to come up. `docker compose logs puter` will tell you which dependency rejected it (most often DB password mismatch between `.env` and `config.json`).
**Login screen says "admin password not set".**
First-boot temp password is logged once. Find it: `docker compose logs puter | grep "tmp_password"`. After login, change it in Settings.
**Healthcheck reports unhealthy but the site works.**
The healthcheck hits `puter.localhost:4100/test` from inside the container. If you changed `domain` or `port`, the check still uses defaults. The site itself is fine.
**Nothing resolves at `puter.example.com` after DNS changes.**
DNS propagates slowly. `dig puter.example.com` and `dig api.puter.example.com` should both return your server IP. If not, give it 5–60 minutes.
**`docker compose up` hangs at "waiting for service to be healthy".**
`docker compose ps` shows which container is unhealthy. MariaDB takes ~20–30s on a cold boot; everything else under 5s. If something stays unhealthy, `logs <service>` will tell you why.
**`Error: DynamoDB aws config requires both access_key and secret_key`.**
You wrote `accessKeyId` / `secretAccessKey` under `dynamo.aws`. That config block uses snake_case (`access_key` / `secret_key`). Only the `s3.s3Config` block uses camelCase.
Loaded 100 of 2509 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.