Commit Graph
759 Commits
Author SHA1 Message Date
Juan Castro c024e007aa Merge juancastro/put-2258-push-from-app-subdomains into juancastro/put-2262-web-push 2026-10-28 16:30:45 -04:00
Juan Castro eeedeb817f Merge juancastro/put-2172-notifications-settings into juancastro/put-2258-push-from-app-subdomains 2026-10-28 16:30:42 -04:00
Juan Castro e8688d5025 Merge juancastro/put-2167-notification-written-hook into juancastro/put-2170-puterjs-notifications 2026-10-28 16:30:35 -04:00
Juan Castro 11a94ed608 Merge juancastro/put-2251-let-apps-send-notifications-to-their-own-user into juancastro/put-2168-let-users-decide-which-apps-can-send-them-pushes 2026-10-28 16:30:30 -04:00
Daniel Salazar 179ad990d6 feat(events): simplify onPersistent delivery (#4395)
* feat(events): simplify onPersistent delivery

Replace delivery/targets with a boolean single. Without single, every
event runs the app's published handler in its events worker and is also
sent to every connected client; the server no longer routes between the
two. single keeps leasing each event to one consumer that acks.

delivery: 'single' | 'broadcast' is still accepted and targets is
ignored. Stored rows are read by their delivery column alone.

* docs(events): compare onLocal, onPersistent and single

* feat(events): rename single to queue

onPersistent({ queue: true }) is the work-queue mode: each event goes to
one consumer at a time and is offered again until acknowledged.
delivery: 'single' is still accepted as an alias, and subscription
objects report queue instead of single.

The docs explain the three modes (onLocal, default onPersistent, queue)
and each events recipe says which one it uses and why.

* docs(events): retries are the only repeat for a queued event

* feat(events): queue deliveries fall through to the worker on decline

A queued delivery gets one socket attempt under a 15 s lease instead of
two 60 s ones. A client with no handler for it, or whose handler fails,
sends events.nack and the worker takes it without waiting for the lease.
Declines are relayed to the region holding the lease like acks are, and
a decline for a lease that is no longer the client's is ignored.

* feat(events): drop thrown errors, retry only temporary ones

A queued handler that throws now drops the event and counts one failure
toward the pause. Network errors, timeouts, rate limits, 5xx responses
and errors thrown with retry: true are retried with the existing
backoff. One classifier is shared by puter.js and the events worker
runtime, which inlines it at build time. A dropped event is billed once,
like a completed one. ack() remains as an optional early release.

* docs(events): queued events reach every tab with a handler
2026-10-28 12:58:51 -07:00
Juan Castro d587ffa428 fix(puter-js): subscribe once the worker is active, and retry a subscription the push service hasn't settled 2026-10-28 13:50:19 -04:00
Juan Castro eb1ad240f2 docs(notifications): describe the confirmation step as what happens; one-line comments and neutral fixtures 2026-10-28 10:06:55 -04:00
Juan Castro d40cc499f5 fix(notifications): enable() rejects on a revoke, onPush() syncs quietly, one permission check per app a minute
enable() no longer opens Puter's prompt after a push_revoked error; apps call requestPermission() from a click and enable() again. onPush()'s page-load sync no longer reports push_unavailable or push_revoked. Hidden-tab banners for an app's messages check the app's push permission once a minute instead of once per message.
2026-10-28 10:06:55 -04:00
Juan Castro 06ef9506fa Merge juancastro/put-2258-push-from-app-subdomains into juancastro/put-2262-web-push 2026-10-28 10:01:02 -04:00
Juan Castro 5513c23042 Merge juancastro/put-2172-notifications-settings into juancastro/put-2258-push-from-app-subdomains 2026-10-28 09:46:54 -04:00
Juan Castro 352a2fbabd Merge juancastro/put-2167-notification-written-hook into juancastro/put-2170-puterjs-notifications 2026-10-28 09:45:45 -04:00
Juan Castro 8482ff2167 Merge juancastro/put-2251-let-apps-send-notifications-to-their-own-user into juancastro/put-2168-let-users-decide-which-apps-can-send-them-pushes 2026-10-28 09:45:31 -04:00
Daniel Salazar 1b6de39a13 feat(notifications): enable() asks through Puter's prompt after a revoke
When the user turned an app's notifications off in Puter, the browser's
permission on the app's origin no longer re-enables them. enable() asks
again through Puter's permission prompt and retries once.
2026-10-28 01:17:00 -07:00
Daniel Salazar 40253ae12b feat(notifications): onPush and enable with Web Push on the app's own origin
puter.notifications.onPush(key, handler) handles an app's notifications for
one channel: live over the events socket while the app is connected, and as
a browser push otherwise. enable() asks for notification permission on the
site's origin and subscribes through the site's worker, which imports
push-sw.js (served at js.puter.com/v2/push-sw.js). A new subscription is
confirmed by a challenge push the worker hands back to the page. disable()
removes it.

The GUI uses the same flow through /sw.js when the server publishes
pushWebPushKey.
2026-10-28 01:05:36 -07:00
Juan Castro fe58213bb5 fix(gui): one enable-push dialog at a time, only for apps that may send pushes
The desktop and the popup show the enable-push dialog only when the
asking app holds notifications:push. The IPC branch answers a second
call unavailable while a dialog is up, as the feedback dialog does. The
silent registration on boot and app launch runs once at a time. A popup
whose opener cannot be attested answers the enable-push request before
it closes.
2026-10-27 16:48:07 -04:00
Juan Castro 415c179770 feat(sdk): puter.notifications.enablePush() and the enable-push popup
An app on its own domain can offer to turn push on in the user's browser
from its own button: puter.notifications.enablePush() opens a popup on
the Puter origin whose single button supplies the click the browser's
prompt needs, and inside the desktop the same dialog is shown over IPC.
The popup hands the opener no token. Docs for the new call and for the
paths that now call the push client's enable().
2026-10-27 16:27:23 -04:00
Neal Shah 25a574b57d GUI for custom domains (#4355)
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
* PUT-2226

* modifications'

* routing fixes

* secondary custom domain verification
2026-10-27 15:54:02 -04:00
dependabot[bot] 0167a9d10e chore(deps): bump fast-uri from 3.1.7 to 3.1.9 in /src/puter-js (#4336)
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.7 to 3.1.9.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.7...v3.1.9)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-27 08:54:47 -07:00
Juan Castro dfa6f4effc Merge juancastro/put-2167-notification-written-hook into juancastro/put-2170-puterjs-notifications 2026-10-27 10:44:19 -04:00
Juan Castro 9fb66137a3 Merge juancastro/put-2251-let-apps-send-notifications-to-their-own-user into juancastro/put-2168-let-users-decide-which-apps-can-send-them-pushes 2026-10-27 10:44:14 -04:00
Daniel Salazar 6c78d2f9e9 fix(ui): reject failed desktop IPC requests (#4378) 2026-10-26 22:14:19 -07:00
Daniel Salazar e8aa12ace5 fix(puter-js): replay requests with fresh body credentials (#4376) 2026-10-26 22:10:24 -07:00
Daniel Salazar f1964778b4 fix(puter-js): coordinate sign-in callbacks and filesystem sockets (#4375) 2026-10-26 22:09:52 -07:00
Daniel Salazar b7a0276337 fix(fs): preserve paging and file read-copy contracts (#4367) 2026-10-26 22:03:09 -07:00
Juan Castro e181a25f95 feat(puter-js): device calls report push_unavailable without a push service
A deployment with no push service has no /push routes, so core answers
the device calls with its catch-all 404. registerDevice, listDevices and
removeDevice turn that into push_unavailable, kept apart from a missing
device, which comes back as device_not_found.
2026-10-26 16:36:46 -04:00
Juan Castro 5e376bc0c8 feat(puter-js): puter.notifications
An app notifies the user it runs for, reads its own notifications back
and follows new ones, hears clicks on them from the desktop, and asks
for or checks the notifications:push grant. Puter's own apps also
register, list and remove the user's devices.
2026-10-26 16:36:42 -04:00
Juan Castro 9fabd1d5b3 Merge juancastro/put-2251-let-apps-send-notifications-to-their-own-user into juancastro/put-2168-let-users-decide-which-apps-can-send-them-pushes 2026-10-26 16:28:52 -04:00
Neal Shah 21625de22e Merge pull request #4315 from HeyPuter/codex/kimi-k3-1-pro
Codex/kimi k3 1 pro
2026-10-26 21:01:04 +01:00
Juan Castro c79b3edf4f Merge juancastro/put-2251-let-apps-send-notifications-to-their-own-user into juancastro/put-2168-let-users-decide-which-apps-can-send-them-pushes 2026-10-26 13:12:24 -04:00
Daniel Salazar f850155411 KV share handles: hardening, higher limits, batch share, uninstall data deletion, account-deletion cleanup (#4344)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
* fix(kv): sign share handle ids and drop retired handles from every node's cache

New handle ids carry a short HMAC (kvh-<uuid>.<mac>); an id whose mac
does not verify is refused before any lookup. Unsigned ids keep working
but no longer fall back to the primary on a replica miss.

Retiring or changing a handle now broadcasts outer.pubsub.kv.share.forget
so every node drops its cached row. Event subscribes and delivery
rechecks refuse a handle whose owner is suspended, as the driver does.

* fix(kv): an app its user removed stops reaching shares

An app acting for a user exercises a share grant only while the user
still has the app authorized: at least one user-to-app permission row,
which every app token path leaves and revoke-user-app '*' removes. The
share itself stays; authorizing the app again restores access. The
answer rides the permission cache, keyed by the app-under-user
generation the revoke bumps, so live subscriptions go quiet too.

* feat(kv): list keys through a watch-level share handle

list with as: 'keys' through a handle needs only watch, which already
learns every key written in the region. Values and entries still need
read.

* feat(kv): filter share listings by app and by state

GET /kv/shares and GET /kv/shares/with-me take an optional appUid: an
app uid, or null for the app-less store. An account session may name
any app; an app is refused any namespace but its own. GET /kv/shares
also takes state=live|revoked|all, all by default.

* feat(puter.js): appUid and state options on the KV share listings

listShared() and listSharedByMe() pass appUid (null for the app-less
store), and listSharedByMe() passes state. Docs cover the new options
and that a watch share can list keys without their values.

* feat(kv): indexes and store counts for share-handle quotas and retention

Adds covering indexes for the owner's live count, the owner-holder pair,
app-filtered listings and retired rows, plus store methods to count a
pair's live handles and delete rows retired before a cutoff. The live
handle cache holds up to 50,000 entries; the miss cache stays at 10,000.

* feat(kv): higher share-handle caps, a per-holder cap, and sharing with several recipients

Live share handles per account go to 25,000 paid / 5,000 free, and per
app to 10,000 / 2,000. One owner may hold out 256 live handles to one
holder on every plan, which bounds the rows a revoke or unshare walks.
Re-sharing an existing region takes no slot and is never refused.

POST /kv/shares takes recipients (1 to 50) as an alternative to
recipient: one region, level and label for all. Caps and a new
per-recipient grant budget (300/min, 2,000/hour) are checked for the
whole batch up front; failures about one recipient are reported on its
item. The single-recipient request and response are unchanged and run
through the same code path.

* feat(kv): delete share handles 30 days after they stop

A daily job deletes retired share-handle rows older than 30 days, in
batches of 500. One node runs it per day; it stops on shutdown.

* feat(puter.js): share a KV prefix with several users in one call

puter.kv.share() takes an array of recipients, positionally or as
recipients in the options form, and resolves to one item per recipient:
the share, or an error with its message and code. The single-recipient
forms are unchanged.

* feat(kv): delete an app's data on uninstall, and close account-deletion gaps

Uninstall:
- POST /auth/revoke-user-app takes an optional `deleteData` boolean, only
  with `permission: '*'` (400 otherwise). AuthService.uninstallApp revokes
  every grant, deletes the app's worker sessions, then awaits
  `app.uninstalled` { userId, userUuid, appUid, deleteData }.
- With deleteData, KvShareService retires the handles the user shares out
  of that app's namespace (as the platform, in batches) and then empties
  the namespace in the background, 5,000 items per step, stopping on
  shutdown. Handles shared with the user are not touched.
- SystemKVStore.purgeNamespace(namespace, { maxItems }) deletes up to
  maxItems items of one `v1:<uuid>:<appUid>` namespace, reading keys only,
  with no mutation events or usage, and reports `done` once it is empty.
  It refuses the system namespace and anything else. flush shares its
  page-delete path.
- puter.perms.revokeApp(appUid, '*', { deleteData: true }); the desktop's
  uninstall dialog has an unchecked "Also delete this app's data" option.

Account deletion:
- EventsService settles each of a deleted account's apps on `user.delete`
  (they cascade without `app.changed`) and drops the owner's subscription
  keys.
- The flat permission entries the account held are purged in pages.

Suspension:
- `user.suspended` / `user.unsuspended` events; the share-handle store
  drops cached rows owned by that user here and on other nodes.

* fix(permission): await a peer's flat-permission deletes so emitAndWait sees them applied

* fix(kv): strongly consistent purge pages, and an uninstall purge stops once the app is authorized again

A stale empty page could end a purge as done while items remained. The
background purge after an uninstall now checks before each step that the
user hasn't authorized the app again, so data written after that is kept.

* fix(kv): flush keeps eventually consistent page reads; only a purge reads strongly consistent

* fix(user): emit user.suspended and user.unsuspended when the user store changes an account's suspension
2026-10-26 09:43:55 -07:00
Juan Castro 759f1a798d Merge origin/juancastro/put-2251-let-apps-send-notifications-to-their-own-user into juancastro/put-2168-let-users-decide-which-apps-can-send-them-pushes 2026-10-26 11:30:50 -04:00
Daniel Salazar 3160b99f11 chore: bump ver (#4335) 2026-10-25 08:24:40 -07:00
Daniel Salazar 589a98b12c feat(kv): share KV prefixes with other users at watch/read/write, and bound what apps can be granted (#4331)
* feat(kv): share key-value prefixes with other users, and bound what apps can be granted

- KV share handles move out of Events into KvShareService, served at /kv/shares
  (the /events/kv-handles routes stay as an alias).
- Shares have watch, read and write levels, encoded in the permission root
  (kv-share, kv-share-read, kv-share-write) with write implying read and watch.
- Every puter.kv method takes optConfig.handle; keys are relative to the shared
  prefix, cursors are sealed, and private entries stay hidden.
- New puter.kv.share, unshare, listShared and listSharedByMe; optional label;
  kv.share.received notice in the app's mailbox only; recipient blocks apply.
- puter.kv.set gains onlyIfAbsent.
- fs and kv events gain viaWorker, and kv events report self from the writer.
- /auth/grant-user-app only accepts canonical permissions from a fixed set of
  families; the consent dialog, puter.perms and docs follow.
- Migrations: kv_share_handles gains revoked_by, label, and grantee and app
  indexes (mysql_mig_49, postgres_mig_38, sqlite 0095).

* fix(kv): never name a private key in a cross-app or handle cursor, and bound flat-permission scans

- Listings of another namespace (cross-app or through a handle) resume after
  the last entry they returned, and their cursors are sealed, bound to the
  namespace or handle, and padded to one length. Old plain cross-app cursors
  are still read when they name the target namespace; other namespaces get 400.
- PermissionStore scans flat permissions a few roots at a time and skips
  roots nested under another root in the same call.
2026-10-25 08:16:19 -07:00
404oops cbcbdc2599 Merge branch 'fix/replayed-tool-call-arguments' into codex/kimi-k3-1-pro 2026-10-24 22:57:54 +02:00
Filip KujundžićandClaude Fable 5.1 bbb780eb31 feat(ai): MiniMax video generation (H3 and Hailuo) (#4309)
* feat(ai): MiniMax video generation (H3 and Hailuo)

Adds a minimax-video-generation provider to the video driver. H3 models go
through MiniMax's v2 task API and are billed per second of output; Hailuo
models go through the v1 API, exchange a file id for a download URL, and
are billed per clip. The shared `minimax` chat key is reused, with the
chat block's `/v1` base URL reduced to its host.

WIDTHxHEIGHT sizes now map onto tiers for catalogs that include a 2k or
4k tier.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(ai): validate MiniMax video inputs and resolution tiers

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 13:49:21 -07:00
Juan Castro 65b966c264 feat(push): notifications:push app permission
Apps need a per-app notifications:push grant for their notifications to
go out as pushes. hasPushConsent checks it as the app acting for the
user, so only that user's grant to that app answers yes, and a revoke
takes effect on the next check. The permission dialog describes it and
the perms docs list it.
2026-10-24 16:03:05 -04:00
Toshit ChawdaandClaude Opus 5 bdc9aa9e93 Revamp networking api with epoxy try 2 (#3476)
* revamp networking api with epoxy

* update to new epoxy build

* force wisp auth

* emit Error objects from socket 'error' events

main tightened the socket error contract to pass an Error (see
types/modules/networking.d.ts, which documents the reason as
`error.message`); the epoxy rewrite still emitted a bare string,
matching the older `(reason: string)` declaration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test: cover the epoxy-based networking client

Replaces the coverage lost when the wisp implementation was removed:
PSocket.test.js and PWispHandler.test.js were bound to PWispHandler,
parsers.js, and wispInfo, none of which survive the epoxy rewrite.

  PSocket.test.js  - connect/retry, inbound data, write normalisation,
                     close, and the TLS event remapping. Driven through
                     real ReadableStream/WritableStream pairs so
                     reader locking, cancel, and abort behave as they
                     do in the browser.
  index.test.js    - relay-token exchange (auth header, 401 re-auth and
                     retry, malformed responses) and the epoxy client
                     cache (keying, refresh, in-flight sharing).
  requests.test.js - pFetch delegation, cache invalidation, and the
                     api call logger's request description.
  epoxy.test.js    - the hand-packed wisp password extension payload.

The wasm runtime loader is left to integration coverage; it needs a
network fetch and a browser. createPuterPasswordBuilder is exported so
the byte layout can be tested against an injected fake runtime.

One skipped test records a pre-existing defect: on a failed write,
#readLoop's normal-termination path races #closeStreams and emits
close(false) after an error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: report socket close flags and epoxy init failures accurately

PSocket #readLoop raced the teardown it was being torn down by. A failed
write sets #closing and hands the close event to #closeStreams, which has
to await reader.cancel() and writer.close() before emitting close(true).
That cancel resolves the loop's pending read with {done: true}, so the
loop broke, reached its own #emitClose(false), and set #closed first --
callers were told the socket shut down cleanly right after an error.
The loop now defers the close event whenever a teardown is under way, in
both its normal and its throwing path, and #closeStreams always emits.

getEpoxyClient swallowed every init failure and resolved undefined, so a
dead relay reached callers as "cannot read properties of undefined
(reading 'connect')" instead of its cause. It now re-throws, still
without caching the failed attempt. A socket reports "wasm unavailable"
or "Failed to create relay token (HTTP 503 ...)" and closes with the
error flag set.

Two related cache faults fell out of that rewrite: the in-flight
early-return ignored `refresh`, handing a caller the very attempt it
asked to replace, and it ignored the cache key, so an attempt started
before a token change satisfied a request made after it. Reuse is now
conditional on both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* restore the public API surface dropped in the epoxy revamp

The epoxy rewrite kept the shape of the old networking client but lost
three things callers depend on:

- JSDoc on the exposed methods. Re-added on PSocket (constructor, on,
  addListener, write, close), PTLSSocket and pFetch, along with the
  SocketEvent typedef and a Networking type on netAPI. `on` carries the
  TLS event aliasing docs that used to live on PTLSSocket, since that
  behaviour moved here.

- socket.write's error message, which lost its trailing '!!'. Apps match
  on the message, so it is restored verbatim.

- pFetch's client-side validation. Epoxy reports a bad request as an
  opaque wasm value from deep inside the transport, so the scheme check,
  the Content-Length/body check and the malformed-request TypeError are
  made before a relay is dialled, rejecting with exactly what they
  rejected with before. Validation builds its Request from a clone, or it
  would consume the body epoxy is about to send.

The latter two are what the net API suite asserts on node, browser and
workerd, and it has been failing on all three since the revamp.

addListener takes named parameters so it can be typed, and three unit
tests that pinned pFetch's habit of forwarding unvalidatable input were
updated to the documented behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* update epoxy client to 04e4930

connectionPrefs changed shape: the handshake and its required extension
list are one object now rather than a tuple, and handing the new build
the old form throws part way through the handshake.

The bump is not optional. The previous build cannot reach the current
relay at all: it enforces the required puter password extension even when
a relay answers with a v1 CONTINUE, and fails with "Protocol extensions
not supported" -- which is what puter.cafe answers with today.

Verified against a local relay: fetch, POST bodies, https, raw sockets and
TLS sockets, the last exercising connectTls, whose third parameter became
an options object in this build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* bump epoxy

* dont require ext

* chore(puter.js): update Epoxy runtime to 68c6904

* chore(puter.js): update Epoxy runtime to affd9f5

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-23 16:57:25 -07:00
Filip Kujundžić 53c77c8333 feat(ai): support Kimi K3.1 Pro models 2026-10-23 20:43:35 +02:00
Rasad RegmiandDaniel Salazar acd3df8c1b fix(puter-js): replay the request after a successful token_auth_failed reauth (#3954)
release-please / release-please (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
* fix(puter-js): replay the request after a token_auth_failed reauth

A 401 carrying the legacy `token_auth_failed` code opened the sign-in
prompt and then dropped the re-authenticated token on the floor:
`resolveReauth()` fell through to `return null` instead of returning
`{ action: 'replay' }`, so the original call was never re-issued and
rejected with `Unauthorized` even though the new session was valid.

Return the replay verdict the `reauth_required` branch already returns.
Both replay paths stay one-shot (`ctx.done.add('reauth')`,
`spec._replayed`), and the auth gate rejects before the route handler
runs, so a replay has no side effects.

Closes #3953

* fix(puter-js): sign in once when a replayed XHR is refused again

handle_resp ran the reauth flow on the replay's own 401, so a
token_auth_failed that survived the replay reset the fresh token and
opened a second sign-in before rejecting. Treat a replay's 401 as final,
as the 403 verification-gate branch already does.

---------

Co-authored-by: Daniel Salazar <daniel.salazar@puter.com>
2026-10-23 00:07:56 -07:00
Daniel Salazar 8b8dd19eae fix(teams): refuse to boot when teams_enabled has nothing registering Teams (#4300)
* fix(teams): refuse to boot when teams_enabled has nothing registering Teams

Core's seat checks (team 2FA, seat password reset, email and username
changes, self-delete, the signup placeholder claim) read the team store and
skip when it is absent. With `teams_enabled` on and no extension registering
the store and service, the server now fails at boot instead of serving
without those checks.

- whoami no longer string-matches the extension proxy's "missing property"
  error: with the flag on, the store is always there.
- Tests that turned the flag on without Teams drop it; the puter.js harness
  sets it only under the `teams` capability.
- Drop a stale services/index.ts comment.

* refactor(gui): drop the Teams console styles, which ship with the console now
2026-10-22 15:11:36 -07:00
Juan Fernando Castro 1627650abc refactor: make teams an optional feature (#4248)
release-please / release-please (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
* refactor: let core run without a teams registration

Teams becomes optional: `services.team` and `stores.team` may be absent, and
core stops assuming either is there.

Seventeen call sites across share, signup, account deletion and the homepage
now degrade instead of throwing: a team recipient that cannot be resolved is
"no such recipient" rather than a fault, a group share folds in nothing, and
account deletion stops asking what a seat was charged for. This generalises
the pattern `gates.ts` and `SocketService` already used.

Teams' own files take a private handle to their store and service, so the
feature's code stays free of optionality that only core needs.

No behaviour changes where Teams is registered.

* refactor: put a contract between core and teams

`core/teams.ts` holds the shapes core reads and the calls it makes, and the
registries declare the pair as provided rather than built in. The service,
store and controller are registered by whatever runs the feature.

Typing them properly turned up eight AuthController call sites the previous
commit could not see: the controller reads its stores from the built-in
registry, so an absent `team` fell to an index signature and typed as
`unknown` instead of erroring. They now go through the same optional path as
the rest.

The feature's own tests move with it. The cases in core suites that build a
team to exercise a core rule still need relocating, so those suites are red
here.

* test: stub core's seat rules, rehome the rest with the feature

A core suite can no longer build a team, so the rules that refuse a seat get
`testFixtures/seatStub.ts` instead: it answers the contract, which is all
those rules read, and puts the registry back when the test ends.

Seventeen cases move that way. Four could not: group shares and a seat's 2FA
enrolment over HTTP need the real feature and the `/teams` route, so they go
with it, as does the whole of `GroupGrants.test.ts`.

Two whoami cases asserted a seat's team through `extension.import`, which
resolves the registry rather than the stores object. They now assert the
absent case, and the behaviour they covered is asserted against
`buildUserDetails`, which takes its stores as an argument.

Suite: 12252 passed, 38 skipped.

* refactor: take the teams console out of the built-in tabs

`TabTeams`, `teamsConsole` and `teamActionIcons` are no longer built in. The
dashboard already invites a tab through its `dashboard-will-open` event, so
whatever provides Teams contributes the console the same way.

Its 113 strings go with it and register into the shipped English dictionary
at load, so `i18n('teams_*')` resolves as it did. Only `en.js` ever carried
them, so no translation is lost.

Three modules stay, because things other than the console use them:
`teamBadge` (the dashboard shell), `orgSeat` (Home, Account, Security) and
`shareTeams` (the share picker). They already render nothing when the
account has no team. `teams_account_of` stays with `teamBadge` for the same
reason, and the `share_*` and `plan_team_*` strings stay with their callers.

The GUI builds clean with no console at all, and builds it in when the tab
is contributed.

* docs: seats past the owner's current limit get no allowance
2026-10-22 15:32:39 -04:00
Daniel Salazar b3c85c86e9 feat(puter.js): open the upgrade page with what was refused (#4294)
The web upgrade dialog's button opens the dashboard with the refusal
reason, the app the call ran as, and the refused method as query params,
so the dashboard can open its upgrade flow with that context.
2026-10-21 13:09:02 -07:00
Daniel Salazar d22d419c4d chore: move to Node 26 (#4295)
* chore: move to Node 26

Docker image and CI run on Node 26 (bookworm-slim, same Debian base as before).

Node 26's bundled undici reads the global dispatcher from the
`undici.globalDispatcher.2` slot, so the guard's 10-minute timeout wrap on
`.1` no longer reached plain fetch() and provider calls fell back to the
5-minute headers timeout. The guard now wraps every present slot and tracks
wrapped dispatchers in a set, since compose() can return a Proxy that writes
a marker property through to the original dispatcher.

* test(puter-js): disable Node's web storage in test workers

Node 26 defines its own localStorage global, undefined without a storage
file, and vitest's jsdom environment doesn't replace globals that already
exist, so jsdom tests saw localStorage as undefined.
2026-10-21 13:04:23 -07:00
Kailasnath TandDaniel Salazar 75385501a7 fix(fs): support offset and byte_count in read operations (#4266)
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
* fix(fs): support offset and byte_count in read operations

Allow partial content reads via offset and byte_count parameters in puter.fs.read and backend FS controllers.

- In puter.js read operation, check options.offset and options.byte_count against null/undefined so offset: 0 is preserved in query params.
- In handle_resp, accept any 2xx status code so 206 Partial Content responses resolve successfully.
- In backend FS controllers (LegacyFSController and FSController), derive the HTTP byte range from query offset/byte_count parameters when an explicit Range header is not present.
- Add unit and integration tests across controllers and SDK test suites.

* fix(fs): strict range params, 416 past EOF, drop format churn

- resolveReadRange accepts only decimal digit strings and safe integers
- readContent maps S3 InvalidRange to 416 instead of a 500
- revert unrelated union-type reformatting, restore truncated test comment

---------

Co-authored-by: Daniel Salazar <daniel.salazar@puter.com>
2026-10-20 21:20:51 -07:00
Daniel Salazar 3bce3b056e misc: hardening (#4290) 2026-10-20 17:12:13 -07:00
dependabot[bot] 356e7b2b94 chore(deps): bump follow-redirects in /src/puter-js (#4288)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.16.0 to 1.16.1.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.16.0...v1.16.1)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-version: 1.16.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-20 16:29:27 -07:00
jelveh f02b4a6b73 feat: tell apps when a launched item is a folder; Open with AI above Open With
A folder opened with an app (Open With, Open with AI) reached puter.js as
a file: the GUI never forwarded the signature's fsentry_is_dir, so the
launched FSItem always had isDirectory false. The GUI now appends
puter.item.is_dir, and puter.js reads it in onLaunchedWithItems and the
onItemsOpened launch replay. Both now share one URL-to-FSItem builder.

"Open with AI" moves directly under Open, above Open With, in the
desktop and dashboard file menus.
2026-10-20 16:16:20 -07:00
242a654166 feat(ai): add Kling for video, image and speech generation (#4274)
* feat(ai): add Kling for video, image and speech generation

One `kling` API key now registers a provider on three drivers:

- txt2vid: Kling 3.0 Turbo, 3.0, 3.0 Omni, O1, 2.6 and 2.5 Turbo,
  with first/last frames, reference images on the omni models, native
  audio, and editing a source video on 3.0 Omni and O1. Billed per
  second at the resolution, audio and video-input rate; an edit is held
  at the longest source and billed for the clip Kling returns.
- txt2img: Kling Image 3.0, 3.0 Omni, O1 and 2.1, billed per image.
- txt2speech: Kling TTS with its English and Chinese voices, billed per
  request.

Kling's business error codes map onto the driver error contract, so an
exhausted resource pack reports as credit exhaustion and a submit-time
content block as `moderation_flagged`. "Risk control" failures now
count as content-filter refusals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ai): treat Kling's unknown-model codes as a Puter-side failure

Kling answers 1202/1203 when the method or model isn't one it serves.
Puter picks both, so these meant catalog drift but surfaced as a 400 that
blamed the caller and raised no alarm. They now return 502 upstream_failed
with a warning alarm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ai): tighten Kling input checks and error mapping

- 3.0 Turbo takes at most 2,500 prompt characters with a first frame,
  the image-to-video endpoint's limit.
- O1 refuses reference images together with a first and last frame.
- 3.0 Omni accepts a source of up to 15.5 seconds, and an edit is held
  at 16 seconds to cover it.
- A source video Kling can't decode fails as 400 upstream_bad_request
  instead of a Puter-side 502.
- A 1201 about the model counts as catalog drift, like 1202/1203.
- Auth failures no longer pass Kling's console wording to the caller.

Adds integration tests for the image, video and TTS providers, gated
on PUTER_TEST_AI_KLING_API_KEY.

* feat(ai): wait up to 14 minutes for a video job

Video jobs, Kling edits in particular, can take over 10 minutes. The
poll window and the Gemini Omni deadline move to 14 minutes, under the
SDK's 15-minute idle timeout, with holds and signed source links
lengthened to match. The keep-alive default moves to 920 seconds so it
stays above a 15-minute proxy idle timeout.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Daniel Salazar <daniel.salazar@puter.com>
2026-10-20 12:32:19 -07:00
Reynaldi Chernando 2a49689fad fix contradicting jsdocs and docs (#4282)
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
2026-10-20 18:39:49 +07:00
e83e27f306 feat(ai): edit a source video with txt2vid (Gemini Omni, FLUX 3) (#4272)
* feat(ai): edit a source video with txt2vid on Gemini Omni

txt2vid takes an `inputVideo` option (sent as `input_video`): a URL, data
URI or Puter file path to an MP4, QuickTime or WebM video of up to 32 MiB.
Gemini Omni receives it inline next to the prompt, which describes the
edit or extension. Google enforces the 10-second limit.

Models declare `videoInput` in the catalog, and the driver rejects a
source video for any other model instead of letting the provider drop it.
The credit hold adds 16,000 input tokens for a source video, since
countTokens reports far fewer tokens than the reply bills.

loadFileInput takes an optional `timeoutMs` for web URLs, and cancels
the body of a failed response.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ai): edit a source video with FLUX 3 on Together AI

FLUX 3 takes `input_video` as Together's `media.source_video`. A web URL
goes through for Together to fetch; a data URI or Puter file is read with
the caller's access, capped at 32 MiB, sniffed as a video and sent inline.
Editing is estimated and billed at the 1080p rate of $0.54/s, since FLUX 3
jobs report no cost. Image inputs can't be combined with a source video.

Seedance 2.0 ignores `source_video`, treats `reference_videos` as a loose
reference rather than an edit, and Seedance 2.5 fails on reference videos,
so neither declares `videoInput`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(ai): name FLUX 3 as a video editor in the AI overview

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ai): match Hailuo 02 to Together's price and document estimate billing

Together quotes $0.56 for a Hailuo 02 clip; the catalog had $0.49.

Together's video jobs no longer report what they cost, so every Together
clip is billed at the catalog's highest published rate. The docs said the
charge was the amount Together reports; they now describe the estimate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: enforce video edit durations and propagate source fetch cancellation

* fix(ai): pass media inputs as Puter files and stream source videos

Source videos were read whole into the API process, base64-encoded and
sent twice to Gemini (countTokens and the interaction), which held about
128 MiB per in-flight edit for up to ten minutes.

- Video inputs are a URL, a Puter file path or a data URI of at most
  1 MiB. Together gets a Puter file as a 15-minute presigned S3 URL and
  fetches it itself; Gemini streams it into the request body with an
  exact Content-Length, so it's never held as one string.
- Gemini counts only the prompt and image for a source video, and both
  providers read the source only after the credit hold.
- txt2img and txt2vid image inputs accept Puter file paths, read within
  the caller's access right before the provider call. Existing URL,
  data URI and base64 inputs are unchanged.
- puter.js writes a Blob, File or data URI over 1 MiB to a temporary
  file under .tmp, sends its path, and deletes it once the call settles.
- FLUX 3 edits bill at the 720p editing rate, matching their 1280x704
  output.

* fix(ai): stage inputs after website sign-in, keep images working without a write

- puter.js chose the temporary file path before signing a website visitor
  in, so it fell outside the app's data directory and the upload was
  refused. It now signs in first, then picks the path.
- An image that can't be written (no access, storage full) is sent inline
  as before instead of failing the call. Videos still fail, since a
  large inline video isn't accepted.
- Telling raw JPEG base64 from a path decodes only the first 24
  characters instead of the whole input.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: 404oops <oops404@util.404oops.com>
Co-authored-by: Daniel Salazar <daniel.salazar@puter.com>
2026-10-19 23:33:03 -07:00