mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-08 15:04:33 +00:00
f0f97a08cfda08af45ecb6bfd36a944c485caa83
725
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3160b99f11 | chore: bump ver (#4335) | ||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
8b8dd19eae |
fix(teams): refuse to boot when teams_enabled has nothing registering Teams (#4300)
* fix(teams): refuse to boot when teams_enabled has nothing registering Teams Core's seat checks (team 2FA, seat password reset, email and username changes, self-delete, the signup placeholder claim) read the team store and skip when it is absent. With `teams_enabled` on and no extension registering the store and service, the server now fails at boot instead of serving without those checks. - whoami no longer string-matches the extension proxy's "missing property" error: with the flag on, the store is always there. - Tests that turned the flag on without Teams drop it; the puter.js harness sets it only under the `teams` capability. - Drop a stale services/index.ts comment. * refactor(gui): drop the Teams console styles, which ship with the console now |
||
|
|
1627650abc |
refactor: make teams an optional feature (#4248)
release-please / release-please (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
* refactor: let core run without a teams registration
Teams becomes optional: `services.team` and `stores.team` may be absent, and
core stops assuming either is there.
Seventeen call sites across share, signup, account deletion and the homepage
now degrade instead of throwing: a team recipient that cannot be resolved is
"no such recipient" rather than a fault, a group share folds in nothing, and
account deletion stops asking what a seat was charged for. This generalises
the pattern `gates.ts` and `SocketService` already used.
Teams' own files take a private handle to their store and service, so the
feature's code stays free of optionality that only core needs.
No behaviour changes where Teams is registered.
* refactor: put a contract between core and teams
`core/teams.ts` holds the shapes core reads and the calls it makes, and the
registries declare the pair as provided rather than built in. The service,
store and controller are registered by whatever runs the feature.
Typing them properly turned up eight AuthController call sites the previous
commit could not see: the controller reads its stores from the built-in
registry, so an absent `team` fell to an index signature and typed as
`unknown` instead of erroring. They now go through the same optional path as
the rest.
The feature's own tests move with it. The cases in core suites that build a
team to exercise a core rule still need relocating, so those suites are red
here.
* test: stub core's seat rules, rehome the rest with the feature
A core suite can no longer build a team, so the rules that refuse a seat get
`testFixtures/seatStub.ts` instead: it answers the contract, which is all
those rules read, and puts the registry back when the test ends.
Seventeen cases move that way. Four could not: group shares and a seat's 2FA
enrolment over HTTP need the real feature and the `/teams` route, so they go
with it, as does the whole of `GroupGrants.test.ts`.
Two whoami cases asserted a seat's team through `extension.import`, which
resolves the registry rather than the stores object. They now assert the
absent case, and the behaviour they covered is asserted against
`buildUserDetails`, which takes its stores as an argument.
Suite: 12252 passed, 38 skipped.
* refactor: take the teams console out of the built-in tabs
`TabTeams`, `teamsConsole` and `teamActionIcons` are no longer built in. The
dashboard already invites a tab through its `dashboard-will-open` event, so
whatever provides Teams contributes the console the same way.
Its 113 strings go with it and register into the shipped English dictionary
at load, so `i18n('teams_*')` resolves as it did. Only `en.js` ever carried
them, so no translation is lost.
Three modules stay, because things other than the console use them:
`teamBadge` (the dashboard shell), `orgSeat` (Home, Account, Security) and
`shareTeams` (the share picker). They already render nothing when the
account has no team. `teams_account_of` stays with `teamBadge` for the same
reason, and the `share_*` and `plan_team_*` strings stay with their callers.
The GUI builds clean with no console at all, and builds it in when the tab
is contributed.
* docs: seats past the owner's current limit get no allowance
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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> |
||
|
|
3bce3b056e | misc: hardening (#4290) | ||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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
|
||
|
|
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> |
||
|
|
18b96d529c | fix: misc scan fixes (#4270) | ||
|
|
9ad7b99607 |
fix(puter-js): handle file picker cancel message (#4257)
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
|
||
|
|
e950a87f29 |
Add Gemini Omni video generation with token metering (#4235)
* Add Gemini Omni video generation with token metering * Keep explicit Gemini video model selection within provider * Settle reported Gemini video usage before validating output --------- Co-authored-by: 404oops <oops404@util.404oops.com> |
||
|
|
1cb9dd756a |
Add Gemini transcription with consistent usage settlement (#4234)
* feat(ai): add Gemini unary transcription * Clarify transcription language results and omit pricing * style(ai): keep Gemini transcription sources prettier-clean Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ai-speech2txt): charge Gemini transcriptions that fail after Google answers Google bills an interaction it processed whether or not it completed, so an incomplete, failed or malformed reply was free to the caller and paid by Puter; audio whose transcript overran the output cap could be resubmitted at no cost. Usage is now recorded before the reply is checked, falling back to the counted audio tokens when the reported usage can't be read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Preserve transcript text with optional malformed annotations * Keep speech provider spacing consistent --------- Co-authored-by: 404oops <oops404@util.404oops.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
be16bca1d9 |
Keep Gemini speech instructions separate and validate speech requests (#4233)
* fix(ai): keep Gemini speech instructions separate from transcript * Clarify Gemini speech instructions * style(ai-tts): keep Gemini cost comment prettier-clean Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Validate speech requests and settle reported Gemini usage * Clarify speech settlement for unreadable responses * Reserve the published Gemini speech token budget * Clarify speech accounting for bounded responses * Limit only Gemini TTS text to 3000 characters; charge overrun replies The 3000-character text cap moves from the TTS driver into the Gemini provider, so other providers keep their own limits. The Gemini reply cap rises to 64 MiB, which fits the full 16,384-token audio budget. A reply that still overruns it or times out reports no usage, so it is charged the held budget and the error tells the caller to split the text. --------- Co-authored-by: 404oops <oops404@util.404oops.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Daniel Salazar <daniel.salazar@puter.com> |
||
|
|
e5ca303c50 | PUT-1903 (#4226) | ||
|
|
ff815f730d |
fix: harden token delivery, session revokes, app identity and fs scoping (#4229)
release-please / release-please (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
- relay sign-in popups take the opener origin from the referrer only - session revokes read the primary, batch per user, and tombstone caches in every region - wisp relay tokens bound to the minting session; verify rechecks the account - hosted origins keep app identity with the site owner; deleted app uids recorded in deleted_apps - subdomain cache tombstoned on delete, update, reclaim and app/account cascades - email lookups require an exact address match after the SQL lookup - workers deploy only from files the deployer owns - share_app_data:false enforced at ACL time, plus copy/move/rename/share/recursive listing - kv remove returns no value without read; kv:expire op grant usable - fs errors name the entry instead of the owner's path - shared-by-me drops rows a delegate no longer has authority over - private apps can't move index_url onto another user's origin - GUI: default apps saved and launched by uid; save picker accepts only copy/move; permission dialog waits for focus; Publish as Worker hidden on others' files |
||
|
|
7abd7b4417 | Remove unsupported speaker reference option | ||
|
|
29e0e1e2aa | Clarify speech catalog units and cover ignored transcription options | ||
|
|
526a250c95 | Correct speech catalog pricing and legacy option declarations | ||
|
|
b8bb6292d6 |
Fix webdav locks issue and tighten GUI ipc rules for setWindow* (#4207)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
|
||
|
|
f9cde23937 |
Merge branch 'filipkujundzic/put-1954-elevenlabs-tts-passes-voice-names-through-as-voice-ids-so' into filipkujundzic/put-1907-ai-streaming-buffering-and-error-shape-cleanup
# Conflicts: # src/backend/drivers/ai-chat/utils/inlineImages.test.ts # src/backend/drivers/ai-chat/utils/inlineImages.ts # src/backend/drivers/util/fileInput.ts |
||
|
|
d8f9aff3dd |
fix: follow-ups to #4161–#4203 (rate limits, broadcast, AI routing and billing, fs subtree walks) (#4205)
* fix(rate-limit): count the newest KV rate rows first The KV backend's rate check, peek and concurrent acquire list the key's rows with a limit + 1 cap that the store applies before dropping expired rows, so unswept rows from past windows (oldest keys) could fill the fetch and the live count read as zero. They now list newest first. * fix(broadcast): split webhook flushes under a byte budget, per-peer backlogs Flushes go out in bodies of at most `broadcast.outboundMaxBodyBytes` (default 4 MiB) instead of one POST, and each peer keeps its own backlog of what it missed, capped by count and four bodies' worth of bytes, oldest dropped first, so one failing peer can no longer grow every peer's body past their 50 MB limit. * fix(ai-chat): classify dropped upstream connections as provider failures A connection reset or refusal (SDK APIConnectionError, Mistral's ConnectionError, an undici reset on the cause) carries no status, so a chain of them fell through to 500 internal_error. Attempts now record connectionFailed; a chain of only resets and 5xx answers 400 upstream_provider_unavailable, and a reset counts as an upstream signal in a mixed chain. * fix(ai-chat): keep SDK retries on Anthropic count_tokens The Claude client is built with SDK retries off so the driver can fall back, but count_tokens has no fallback route. It now passes sdkRetryOptions, like the OpenAI moderation call, so a transient 429 or reset is retried. * fix(ai-chat): mark a route unhealthy for 60 s after a transient failure With SDK retries off, one 5xx, 429, timeout or dropped connection kept a route marked for 10 minutes, sending that model's traffic to resellers. Those failures now mark the route for 60 s; bad credentials, an exhausted account and other route-level failures keep 10 minutes. * fix(metering): don't memoise spent addons inside the force-exact window The spent-addons memo could cache a pre-grant record read from a lagging replica right after a credits-changed announcement, so overage skipped the new credit and requireCredits kept answering 402 for up to 15 s. A spent record is now only cached once the actor's force-exact window has ended. * fix(share): send the digest entries a flush claimed when another take fails Digest entries are taken concurrently, and a single rejected take threw away every entry the other takes had already removed from KV. The flush now logs the failed takes and sends what it claimed; an entry whose take failed stays queued for a later flush. * fix(fs): announce an overwriting move's replaced entry only when it took its name /move and v2 /fs/move looked up the entry at the destination name before moving and always announced its removal. A move into someone else's Trash renames instead of replacing, so the owner's desktop hid a live entry and legacy /move reported it as `overwritten`. Both routes now pass the target on only when the moved entry kept the name it was looked up by. * fix(ai): name the vendor in upstream error fields from media providers upstreamFetch now takes the provider id plus a display label, and UpstreamResponseError carries the id. translateProviderError copies a string `provider` off the error into `fields`, and the BytePlus video and Cloudflare image errors set one, so upstream_auth_failed and upstream_credits_exhausted alarms say which vendor failed again. * fix(teams): delete a provisioned seat whose home folders could not be created provisionUser logs a failed default-folder creation and still returns the account, so provisionAccount went on to add the membership, issue a password, mail the seat and emit team.account.created for an account with no home. It now cascade-deletes the new user and returns 500 internal_error before any of that. * fix(ai-chat): load model catalogs without holding up server start onServerStart awaited every gateway catalog (up to its 10s fetch timeout) while HTTP was already listening, so drivers started after it had no providers yet and chat requests found an empty model map. The first build now runs in the background; complete(), models() and token counting wait on it instead of failing. * fix(ai-chat): serve direct models while gateway catalogs load at boot The first model-map build now publishes a fresh map as each provider's catalog lands, so static catalogs are routable at once. complete() and token counting wait for the full first build only when the model, or the provider asked for, isn't in the map yet. Refreshes still swap once. * fix(ai-chat): split gateway-billed cost across input and output A completion billed at the cost OpenRouter, Infron or Neuralwatt reports was quoted to `ai.prompt.cost-calculated` as all input and no output. The ledger still records one `billedUsage` line; the reported per-key costs now spread that bill over the usage keys at the catalog's relative rates, in whole µ¢ that sum to it. Neuralwatt energy units take no share. * fix(ai-image): hold Replicate credits for the whole prediction The output and input-surcharge holds took the default 10-minute TTL, the same as the prediction window alone, so a slow prediction could outlive them once creation and input measurement were added. They now last 15 minutes, like the video job holds. * feat(ai-chat): report what a completion was charged as usage.cost Chat usage (the non-stream result and the stream's final usage line) now carries `cost`: the µ¢ the ledger records for the request after the cost factor, from the request's recorded charges and the gate's factor lookup. `usd_cents` is unchanged. A hooked recording now copies the usage object first, so keys the driver adds later never land in the ledger. * fix(ai): charge streamed speech once its first byte arrives Per-character TTS and the voice changer recorded their charge when the upstream's response headers arrived, so a body that failed before any audio was billed in full. A shared relay now charges on the first byte, or when the caller hangs up, and charges nothing for an upstream that fails or ends before sending audio. * fix(ai): release credit holds only after the request's usage lands A hold was released as soon as its operation returned, while the usage it stood for was still being written (behind the cost-factor lookup), so a concurrent request briefly saw neither. AI metering now tracks the request's usage writes and its holds (chat included) release once those settle, without delaying the response; a streamed charge chains its own. A failed write still releases the hold, and the hold's TTL backstops it. * test(ai-chat): wait for a stream's credit hold to come back after its usage lands * perf(fs): page subtree delete and copy by parent link, not by depth over the path range Each page of a recursive delete or copy sorted the whole remaining subtree by a computed depth, so a walk cost grew with the square of its size. It now pages the children of up to 100 sibling directories at a time by parent_uid, depth first, keyset on (parent_uid, id), so a page reads only its own rows; rows with no stored path are reached too. SQLite gets the parent_uid index the others have. * fix(fs): find children by parent_uid in the empty check and supplementary-name lookup Rows the old backend wrote carry parent_uid and no parent_id, so a non-recursive delete of a directory holding only such rows passed the empty check and orphaned them, and on MySQL the supplementary-character child lookup missed them, leaving their subtrees out of listings, counts and renames. Both now key on parent_uid; the empty check still probes parent_id too, which the delete cascade follows. * docs(fs): drop the single-POST note from the entry broadcast cap |
||
|
|
b88aa55b24 |
fix: drop moved descendants from the entry cache; read cursor=null as the first page (#4204)
Renaming or moving a directory rewrote its descendants' paths but left their
cached copies (by id, uuid and old path) live for up to 60 s. A lookup of an
old path kept resolving, and a write beneath one landed at a path its parent
no longer had. updatePathPrefixForUser and renameUserHome now drop those keys,
here and, within the broadcast budget, in peer regions.
puter.js sent `cursor=null` for `listShared`/`listSharedByMe({ cursor: null })`,
which decoded to null and failed every keyset listing with a 500. decodeCursor
reads it as no cursor, and the SDK no longer sends it.
|
||
|
|
64db150a90 | fix(ai): bound streaming buffers and normalize input errors | ||
|
|
5fb025c109 |
Misc subdomain and worker fixes (#4157)
* PUT 1902 * LocalWorker fetch policy fixes for 127.0.0.1 |
||
|
|
360f6b1079 |
fix(events): bill reached deliveries, bound dispatch reads, seal cursors (PUT-1900) (#4146)
* fix(events): bill reached deliveries only, bound dispatch reads, seal cursors - A broadcast socket copy is billed only when a connection for its holder exists on this node, anywhere in this region (connection count) or, for a durable row, in a region presence names. Without peers it bills as before. - Compiled match filters are held in a bounded LRU with a TTL, and dropped when another process reports a subscription ended. - GET /events/fetch reads the continuation from `cursor`, falling back to `after`. - Cursors from /events/fetch, /events/subscriptions and /events/kv-handles are encrypted with sealCursor/openCursor; plain cursors still read. - A durable subscribe counts again after its insert and removes its own row when over the cap; the generation bump for that removal is published. - Subscribe checks access before reading the anchor's owner, and an anchor gone in between answers with the subject the caller sent. - Dispatch reads at most the rows per token routing can use, scanning large token hashes; a removal still settles every row on the removed anchor. - puter.events.unsubscribe() stops routing after the server ends the subscription or reports it gone; fetch() accepts `cursor`. * fix(events): forward kv values only to regions with rows that ask - A region counts its session rows asking for kv values per token and announces the token's value variant (`v#<token>`) over the existing `watch` item on the first such row, withdrawing it with the last. Add, remove, reap, reanchor and refresh all keep it, reading `includeValue` off the row as it is dropped. - A kv write reads the value variants alongside the watch index and attaches the value only to forwards bound for a region that announced one. * fix(pagination): seal id-keyed cursors on every list endpoint Apps, subdomains, workers, team (members, directory, teams, audit, own audit), shares (inbound, outbound), the user-to-user permission audit and readdir/descendant listings now hand out cursors sealed with sealCursor and read them with openCursor, so a cursor no longer carries a row id. Plain cursors issued before still read. * test(events): pass socket ids to noteConnect in the billing tests |
||
|
|
8ecfeec8c3 |
fix(puter-js): request idle timeout, callback/listener leaks, guarded message handlers (PUT-1908) (#4143)
* fix(puter-js): free one-shot callbacks and listeners, guard message handlers - setAuthToken starts the GUI cache refresh loop once instead of per call - IPC reply callbacks are dropped after their reply; dehydrated callbacks stay - AppConnection removes its window listener when the target app closes - reply branches for app data and instance count drop their callback - UI, Debug, tool and GUI-token message listeners no longer reject; an unknown or failing tool answers toolResponse with an error - parseResponse handles a blob response with no content-type - retry waits remove their abort listener when the wait ends - EmailConfirmationDialog escapes its message - EventListener uses a real Map and snapshots listeners per emit - Context get/set/runWithContext route every known key the same way * fix(puter-js): time out requests that make no progress - buildXhr starts an idle clock on send, reset by state changes and body progress (upload progress only on requests already carrying a bearer) - read-safe requests get 60 s while progress is observable, everything else 15 min; timeout: 0 on fetchUrl/driverCall/sendWithRetry turns it off - a timed-out read replays once; writes and AI calls never replay - timeouts reject with code request_timeout on fetchUrl (TypeError), driverCall, streams and the legacy XHR path - document the limits in rate-limits-and-quotas and the chat stream note |
||
|
|
50e2af34bf |
feat(puter-js): Add custom streams and perfect negotiation to Peer (#4058)
* add perfect renegotiation
* more advanced peer logic
* add video tracks to peer api
* several fixes
* small fixes, revert
|
||
|
|
95b6f86fcc |
fix(events): resolve relative fs: paths like fs operations do (#4085)
* fix(events): resolve relative fs: paths like fs operations do * docs(events): use bare relative paths in examples, test both relative forms * docs(events): clarify that relative fs: subjects come back resolved --------- Co-authored-by: Daniel Salazar <daniel.salazar@puter.com> |
||
|
|
d1d55a6bc3 |
SDK auth plumbing: cache scope, dedupe keys and the token listener (#4112)
* fix: pin the token sender, scope the grant flag, and stop two stragglers
Five of the SDK auth-plumbing items.
**The global `puter.token` handler pinned origin but not source.** Its three
siblings all pin both, and each says why: the GUI origin is shared by every
frame on that domain, so origin alone identifies a domain, not a sender.
`signIn` pins its popup and binds `msg_id`, the consent dialog pins its popup,
and the reauth wait pins the embedder. This one pinned the embedder only when
`env === 'app'`. Since `authenticateWithPuter` now hands off to `signIn`, which
adopts the token itself, the embedder is the only sender this handler is left
serving, so it pins in every environment.
**`is_grant_user_app_permission` was a request-wide flag held across an await.**
It tells the `app-root-dir` rewriter that a row is being written, and while it
is set that rewriter resolves to a real `fs:` permission instead of
`PERMISSION_FOR_NOTHING_IN_PARTICULAR`. Anything evaluating an `app-root-dir:`
permission beside the grant -- the grant route runs one under `Promise.all`
with fs work -- read the same flag. It now lives in a scope derived for that
call, so what the write needs to say about itself is not also said to its
neighbours. `runInDerivedContext` is new: `runWithContext` starts an empty
store, which would drop the `actor` the rewriter reads.
**`fs.stat` and `fs.readdir` deduped on parameters alone**, so two identities
in one realm could share a result. They now key on origin and token as
`os/user.js` does, which documents the reason.
**`signIn`'s relay poll never stopped.** Closing the popup settles the promise;
the loop kept asking `/login/wait` every second for the life of the page.
**`triggerReauth`'s five-minute timeout was never cleared**, so a successful
reauth left a timer to fire into a settled promise.
The concurrency test parks the rewrite and reads the flag while it is held --
my first attempt read before the flag was even set and passed either way.
* fix: scope the fs cache per identity, and retarget two paths that never ran
The remaining SDK auth-plumbing items.
**`puter._cache` outlived the identity that filled it.** `_clearAuthToken`
dropped the token and the stored session but left the cache and `whoami` as
they were, and its keys were raw paths -- `item:/alice/Documents` and a
`~`-relative key alike, identical whoever asked. A GUI user switch without a
reload could be served the previous user's stat or readdir. Keys now carry the
origin and token they were read as, built in one place so the writers, the
readers, the invalidations and the warm-up probes cannot drift apart, and
signing out drops the cache and `whoami` with the token.
**The driver permission prompt could not fire.** Both paths waited for a 200
carrying `{success:false, error:{code:'permission_denied'}}`. The driver route
answers `success: true` or throws, so a refusal arrives as a 403 -- the shape
the retry now reads. Its two tests synthesised the old envelope, so they passed
against a response the API does not send; they use the real one now, and the
prompt goes from never called to called once.
This is a behaviour change: a driver call refused for permissions will now
prompt, which is what the path was written to do. Happy to delete it instead if
the prompt is not wanted.
The half-written copy of the same idea in `utils.js` is gone -- it prompted for
one hard-coded interface and its retry was a `// todo`.
**The verification-gate single-flight was keyed on nothing**, so a phone-gated
request in flight beside an email-gated one took the email answer, replayed,
failed again and surfaced raw without ever asking. Keyed per gate: same gate
still shares one dialog, a different gate gets its own.
* test: ask the cache the same way the code does
The API suites built `item:<path>` keys by hand, so they stopped finding
anything once the real keys carried the identity they were read as. They go
through `fsCacheKey` now, which is the point: a test that spells the key itself
passes until the moment the spelling changes, and then fails for a reason that
has nothing to do with the behaviour it is guarding.
These run against a live server, so a plain `vitest run` does not reach them --
they were green locally and red in CI.
* fix: the review's findings on this branch
Eight, and three of them were regressions this branch introduced.
**Pinning the token sender to `globalThis.parent` in every environment broke
the case it was meant to protect.** On a top-level page `parent` is the page
itself, and the token arrives from a popup, so the handler stopped accepting
anything. The GUI posts `puter.token` to `window.opener` for the file pickers
too, not only for sign-in, and this handler is their only consumer -- a site
calling `showOpenFilePicker()` would have signed the user in, dropped the token
and run the following read with none. It now pins the embedder when framed and
a window this SDK opened when not; `UI.js` already tracked its picker popups
for the same reason, so that set is shared rather than duplicated.
**The GUI still spelled a cache key by hand.** `updateSubdomainsForItems` built
`item:<path>`, which no longer matches what the SDK writes, so its `get` could
not hit and its `set` wrote an orphan nothing reads and nothing evicts -- a
published website's badge quietly stopped appearing on a cached render. It goes
through `fsCacheKey` now, and that is the last hand-spelled key in the tree.
**Retargeting the driver permission prompt to 403 was wrong and is reverted to
the deletion the ticket asked for.** The route refuses with `forbidden`, not
`permission_denied`, so the retarget still missed it; meanwhile a driver that
raises `permission_denied` for its own reasons would have opened a prompt that
had nothing to do with the refusal. And the prompt asks for
`driver:<iface>:<method>` while the route checks `service:<driver>:ii:<iface>`,
so granting it changes nothing. The path cannot work as written.
The rest were pre-existing or latent:
`cacheWhoami_` wrote its answer without checking the token it was issued under
was still current, so a reauth landing mid-flight could repopulate `whoami`
with the previous identity. `_dropIdentityCaches` also left `whoamiCache_`,
which other call sites await.
`setAuthToken` now drops the identity caches when the token actually changes:
keys carry the token and the store is persisted with no TTL, so a rotation that
did not go through `_clearAuthToken` orphaned a whole key space with nothing to
reclaim it.
The verification-gate entry was registered after the body could already have
deleted it -- a synchronous throw stranded a resolved "not verified" in the map
for the life of the page. It registers first and clears on a microtask.
`signIn`'s poll is bounded as well as settle-checked: on a cross-origin
isolated page opened from a gesture, nothing watches the popup, so closing it
never settled the promise and the loop ran on exactly as before.
`runInDerivedContext` says in its docblock that callee writes do not survive;
only the one flag needs the scope, and a future caller should not expect to
hand a value back through it.
* fix: the review's findings on the SDK auth plumbing
**The relay bound stopped asking without saying so.** The deadline left the
`signIn` promise pending and its listener attached. On an isolated page opened
from a gesture the relay is the only thing that can settle it -- nothing watches
the popup there -- so a user who took longer than the deadline was left with a
promise that never resolved, and `authenticateWithPuter` queues every later
implicit-auth call behind it. It rejects now.
**Pinning the token source left `signIn`'s own popup untracked.** Only the
picker popups registered, so on a top-level page the global handler bailed on a
sign-in it had opened itself: `puter.onAuth` never fired, and
`puterAuthState.authGranted` was never set. `openAuthPopup` is the one place
auth popups are opened, so they register there.
**A closed popup is ours too.** Rejecting one that had already closed loses a
message from a popup that posts and then closes itself, which is why `UI.js`
tolerates the same case. Kept, with the set bounded instead.
**The gate key carried the factors**, so one route naming them and another not
opened two dialogs for the same gate. It keys on the gate, as `ctx.done` does.
**`fsCacheKey` left `~` unresolved**, so a writer and an invalidation spelling
the same place differently missed each other -- the drift the one key builder
was meant to end.
The dead `opts.permission` went with the path that used to read it.
Four workerd-runner failures are left: `ai > chat stream adapter supports
cancellation`, `kv > a GUI boot key written by another client reads fresh` and
two cache-warmer ones. All four fail the same way on `origin/main`, and the node
and browser runners pass.
* chore: one-line the comments added here
* chore: reflow the comments the removed retry cause left ragged
|
||
|
|
085ab1774f |
chore(deps): bump shell-quote from 1.10.0 to 1.12.0 in /src/puter-js (#4120)
Bumps [shell-quote](https://github.com/ljharb/shell-quote) from 1.10.0 to 1.12.0. - [Changelog](https://github.com/ljharb/shell-quote/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/shell-quote/compare/v1.10.0...v1.12.0) --- updated-dependencies: - dependency-name: shell-quote dependency-version: 1.12.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
b6150d63f3 |
Merge pull request #4090 from HeyPuter/fix/ai-stream-cancellation
fix(ai): propagate cancellation and stream termination failures |
||
|
|
2638e9ce93 |
fix(kv): batch-read GUI boot keys only when the SDK runs as the GUI (PUT-1899) (#4117)
The boot-key batch now starts only when puter.env is 'gui'. In every other environment a get() of one of those names reads that key on its own, like any other key, instead of fetching all 14 and serving them for 4 s. |
||
|
|
fde323842e |
fix(kv): SDK list rejections, boot-cache read-after-write, byte-accurate size checks (PUT-1899) (#4108)
- puter.kv.list() no longer throws synchronously for invalid options; it returns a rejected promise, or for stream: true an iterator whose first next() rejects. Error codes unchanged. - Writes through puter.kv (set, del, incr, decr, add, remove, update, expire, expireAt, flush) stop the boot-key batch from serving the keys they touch, so the next get() reads the store. Untouched boot keys are still served from the batch. - Client-side key/value size checks measure UTF-8 bytes, and values are measured as their JSON encoding, matching the store. Same codes and messages. |
||
|
|
4a54beac6b |
fix(fs): SDK uid stat cache, readdir page types, move lookup errors, UID-shaped paths (#4109)
- stat: only path-addressed results are cached; uid-addressed stats no longer share one cache key. - readdir: overloads declare the page result for recursive + limit/offset and for the positional page/stream forms. - move: only a not-found destination lookup means "new path"; other lookup failures reject. A uid destination skips the lookup. - getAbsolutePathForApp always resolves paths. read/copy/move/delete use getAbsolutePathOrUidForApp, which keeps sending UID-shaped strings as uids. - Docs for read, copy, move, delete and readdir. |
||
|
|
b8fcbffe3f |
fix: the five remaining review findings
**The path oracle was not closed.** The rewrite only remapped the *error*, so success-vs-403 still answered whether a foreign path exists. Narrower than reported: every other grant path already gates on `canManagePermission`, and only `grantUserAppPermission` has no authority check -- deliberately, since granting your own app something you don't hold is inert. It now refuses a foreign path the actor cannot see, with the same error a missing one gives. My first test for this passed without the fix: `makeUserActor` creates no fsentries, so both paths 404'd and nothing distinguished them. It inserts a real entry now, and the control fails with "expected null to match 403" -- null being the grant that succeeded, which is the oracle. **`#validateEmail` still failed open** on the main signup / change-email / confirm path while its OIDC twin was hardened. Same shape as OIDC now: a gate that did not run is not a pass. **The 16-permission cap broke `puter.perms.check()`** for longer lists. The SDK's own docblock promised "one round trip however many are asked about", which the cap made untrue, so it batches instead. The reported second half -- the UI grant poller burning its timeout -- is not reachable: `MAX_REQUESTED_PERMISSIONS` already caps that path at the same 16. **`listener_failed` was set by wildcard listeners.** An unrelated observer on `email.*` throwing would refuse every signup. Only an exact-key listener implements the event, so only that one speaks for the gate. **`whoami()` labelled everything `Unauthorized`.** A 502 with an HTML body is not a 401; the server's `code` now rides along and the fallback names the status. |
||
|
|
66774ff90c | Merge branch 'main' into juancastro/permissions-access-control-cleanup | ||
|
|
96f0c51d1e | fix(ai): propagate cancellation and stream termination failures | ||
|
|
95809c7dd5 |
fix: bound the permission check, and let whoami fail
`/auth/check-permissions` took an array of any length. `checkMany` batches the
scan so it is not the fan-out the report describes, but nothing stopped a caller
sending thousands of strings; it now takes the same 16 the grant and revoke
routes take.
`whoami()` returned `resp.json()` without looking at the status, so an expired
token resolved to an error body typed as a `User`. Callers read `username` off
it as undefined — which is how a consent prompt came to ask for
`/undefined/Documents`. It throws the documented `{ status, message }` shape
now, the same one the no-token path already threw.
|
||
|
|
acd6ea3c75 |
fix: do not spend a consent prompt on an app root dir that is not the caller's
`perms.request('appRootDir', { app: <someone else's> })` could never succeed —
only the app itself may claim its root dir — but the SDK read the refusal as
"not granted yet", prompted the user, took the grant, then polled until it gave
up and resolved undefined. A prompt the user cannot act on, and a live grant for
a directory the app will never reach.
The route now says which kind of refusal it is. `forbidden` alone could not
carry it: the SDK already treats that as the refusal a grant is about to lift,
and a unit test pins exactly that sequence, so a new `reason: 'app_mismatch'`
field rides alongside instead of changing the code clients read.
An entry refused that way is held back from the pooled grant and resolves
undefined without a prompt, which is what it resolved to before.
|
||
|
|
957784bdb8 |
fix(ai): keep existing chat callers on their previous behavior
Narrows the wire-shape changes to the callers that need them. - request-level upstream 4xx stays strict only on the Anthropic route; /drivers/call and the OpenAI routes fall back on any error again - outage chains return 400 with the old codes; the Anthropic error renderer maps them to 502/529 from the carried upstream status - native Claude results report finish_reason 'stop' again (real reason in stopReason); OpenAI routes still map the real value, and a stream that carried tool calls never ends as a plain stop - Claude usage carries thinking_tokens again, informational only and added after pricing and the funds cap - reasoning_effort is clamped to values each OpenAI model accepts; Hoonify, MiniMax, ZAI and Meta only receive params they support - OpenAI-form tool_choice from /drivers/call is normalized again - /responses accepts background:false; mcp_servers is ignored; legacy media block shapes pass the Anthropic route's validation - OpenAI web search holds one call; only search actions are billed; Azure bills Bing grounding requests; usd_cents includes search fees |
||
|
|
a242e31343 |
feat(ai): carry the full Anthropic and OpenAI wire shapes through the chat driver
The Anthropic-compatible route flattened requests into the OpenAI-style driver shape and rebuilt them for Claude, which dropped system blocks, thinking, tool_choice, cache_control, betas and stop reasons. Claude Code requests 400ed upstream and were silently served by a reseller. - normalized driver args, stream chunks and results gain tool choice, thinking, output format, stop sequences, caching, stop reasons, usage details, reasoning signatures, server-tool blocks and safeguard results - ClaudeProvider builds requests from those fields with a beta, tool and body-field allowlist; meters fast mode, web search and advisor calls, and no longer bills thinking tokens twice - fallback only on route-level failures; outages map to 502/529 - vendor error envelopes on the Anthropic and OpenAI routes - Anthropic route: native SSE with pings, count_tokens, /v1/models - OpenAI-family providers and routes map the same fields; Responses web search is metered; reasoning_effort gating fixed; /responses rejects org-scoped stateful params and forces store:false - puter.js: typedefs, new chat options, silent toString for new chunks |
||
|
|
a46eb15d5e |
Let the account's own API token reach the routes a privileged app needs (#4032)
* feat: let the account's own API token reach the routes it should
A privileged app is launched with the GUI session token today. Running one on a
full-access personal access token instead costs much less when it leaks, but
several routes those apps depend on refuse every access token, so the swap would
break them.
Admitted, each a read or a write the token's own HTTP reach already covers:
- the realtime socket handshake, which refused access tokens outright; FS
live updates and notifications stop without it
- GET /get-dev-profile and POST /profile, both puter.js surface
- GET /auth/list-permissions — a read of what was granted, not a grant
- GET /share/shared-by-me/apps and GET /share/audit, over grants
/share/shared-by-me already lists for the same credential
- POST /rao, which puter.js calls on every setAuthToken
Gated on `isAccountContext`, not the `full_access` claim alone, so a token with
an app anywhere in its chain is still refused and a socket for one never joins
the user room.
Left session-only: mint, session, 2FA, grant, team and billing, plus two the
list named. /share/blocks is a personal safety control whose only caller is the
desktop's Blocked Senders window. /app-feedback is reachable only from our own
GUI pages, and being unsubmittable programmatically on a user's behalf is the
property it was built with.
The new HTTP suite drives a real server with a minted token and fails on all six
admissions without the change; the block routes' existing metadata check already
pins their refusal.
* test: sort the two token shapes at the events handshake
* fix: carry the account-context invariant into the HTTP gate
`requireUserActorGate` admitted on the raw `full_access` claim while the socket
and /rao use `isAccountContext`, so one shape — a token carrying the claim with
an app in its chain, or an actor built without `makeActor` — would have been
refused in two places and admitted in the third. Unreachable today, since the
mint and the auth-time read both drop the claim when `app_uid` is present, but
it is the invariant this change states and it should hold at every enforcement
point.
The payout address goes back behind a plain session: `/get-dev-profile` is open
to the account's own token now, and the rest of what it answers is a developer's
own name and incentive-program status.
Also drops a cross-reference in EventsService to the socket posture this change
relaxed, and rewords the block-routes comment, which read as if access tokens
were admitted there rather than refused.
* fix: drop a revoked token's sockets, and read the dev-profile columns
Two gaps the review found, both reachable only because this branch lets the
account's own token hold a socket and call /get-dev-profile.
`#revokeAccessTokenTail` soft-revoked the session row and told no one, so a
revoked token kept its connection — and the account's `outer.gui.*` fan with it
— until the five-minute reauth sweep. Sockets now join a per-token room and a
new `auth.access-token.revoked` event drops exactly that room, so the account's
other tabs stay up. Session revoke keeps its account-wide eviction.
`/get-dev-profile` read `first_name`, `last_name`, `paypal` and the two
incentive flags off the user row, which carries none of them: the columns have
always been `dev_`-prefixed, so the endpoint answered nulls to everyone and
`puter.apps.getDeveloperProfile()` has never returned anything. Reads the real
columns, keeping the unprefixed name as a fallback for a deployment carrying
both. The payout address stays behind a plain session, so opening the route to
the account's token hands over a name and incentive status and nothing else.
Both fail without the fix: the socket test waits five seconds on a connection
that should already be gone, and the profile test reads back a seeded row.
* fix: close /open_item to tokens, and stop the node runner leaking sockets
`/open_item` carried `allowFullAccessToken`, but it grants an app a permission
on the user's behalf and mints the app token to go with it — a grant and a mint,
which stay session-only. Pre-existing, and missed by the first sweep here, which
only asked whether routes lacking the flag should gain it and never re-read the
ones that already had it. Refused at the top of the handler too, so a credential
that gets past the gate cannot leave an ACL row behind.
The node runner loads each SDK into a vm context and never closed it, so every
test leaked a socket, and every socket holds a per-(user, origin) connection
slot. Admitting the account's own token to the handshake pushed that past the
cap, and the whole events block failed with `events_connection_failed` while the
browser and workerd runners — which do not leak — passed. Closing the FS socket
and the events channel after each test gives the slots back.
* fix: announce only the revokes that can have a socket to drop
Every access-token revoke broadcast an eviction cluster-wide, including the
read-URL tokens `revokeReadUrl` retires constantly. A scoped token is refused at
the handshake, so none of those broadcasts could ever reach a connection.
The announcement now goes out only for a token the handshake would have
admitted: full access, no app in the chain. `revokeOwnAccessToken` refuses
full-access tokens outright, so that whole path is silent; a revoke by raw uuid
says nothing about the token and still announces, which costs a no-op broadcast
rather than leaving a revoked socket up.
Fails without it: the test revokes a scoped token and a full-access one through
the same entry point and counts the announcements.
* test: pin the mint route against the account's own token
`/auth/create-access-token` carries only `requireAuth`, so it never showed up
in the sweep over `requireUserActor` routes — the refusal lives in
`AuthService.createAccessToken`, which turns away any access-token actor
outright. That holds today, and a leaked full-access token minting itself a
sibling that survives revoking the original is exactly what the token swap is
meant to rule out, so it is worth a test rather than a reading.
* fix: let the recommended-apps test follow the list it tests
Two commits on main rewrote `RECOMMENDED_APP_NAMES` without touching the test,
which seeds `editor` and `camera` and asserts the result contains them. Neither
is on the list any more, so the resolved set came back empty and `test (base)`
has failed on every PR since.
The test now seeds the first two names off the list itself and asserts on those,
so the next edit to it cannot strand the test: prepending two unknown apps keeps
all four cases green.
* feat: let an extension waive a plan gate for one route or driver method
A plan gate about to refuse a caller now emits
`subscription.gate.<surface>` through `emitAndWait` first:
`route.<method>.<path>` for a route, `driver.<iface>.<method>` for a driver
method, `share.anyone` for link shares. The payload carries the actor, the
request and the requirement, and a listener that sets `allow` waives the plan
for that call. Callers the plan already covers never reach the hook, and a
listener that throws leaves the refusal in place.
* feat: mint godmode apps their own full-access token
`/auth/get-user-app-token` answers a desktop launch of a godmode app (the
`godmode` column) with a full-access token instead of an app token. Origin
lookups, the sign-in popups for pages outside the desktop, keep getting
ordinary app tokens, and only a web session may mint one.
The token carries the app in `godmode_app_uid`, read into
`accessToken.godmodeApp` for attribution only: `effectiveApp` stays null, so
it keeps the account's reach and passes the same gates as a personal access
token, including the vendor-compatible AI routes. It is refused once the app
is gone or no longer godmode.
Its row is an `access_token` session parented to the web session that asked,
with `app_uid` set, so signing out, revoking that session or revoking all
sessions takes it along, and the sessions list shows it under the app. Each
mint re-signs onto the same row with a 12h `exp` and moves the row's expiry
out, so the desktop keeps an open app alive while a copy taken earlier still
expires on schedule.
A full-access token may now mint scoped tokens (read URLs), parented to its
own row; it still may not mint full access. Revoking a full-access token
cascades to the tokens it minted.
An access token whose session row has a parent now authenticates only while
that parent is live, so a read URL a godmode app minted lapses with the app's
token, and through it with the desktop session.
* feat: launch godmode apps on their own token and renew it
The desktop asks for a godmode app's token at launch and blocks the launch if
it can't get one, as it does for other apps; a godmode app never runs on the
desktop's session token. While the window is open the desktop re-mints the
token once less than 6h of its 12h remain, checking on a timer and when the
tab becomes visible, and answers the app's `reauth_required` with a fresh one,
posted to the app's origin.
puter.js keeps a godmode token in sessionStorage like a session token, never
in localStorage. In app mode a 401 on a godmode token, background requests
included, asks the desktop for a new token and replays, and a `puter.token`
message is only taken from the embedding frame.
The sessions list shows a godmode token row with its app's title and icon.
* fix: keep a godmode token the same across renewals
Apps hand their launch token to code that never sees a replacement (the
terminal's Drive mount, a CLI's API key), so a token re-signed every renewal
broke those copies once the first one expired. The JWT now carries no `exp`
and a renewal moves only its row's expiry, so every copy keeps working while
the desktop keeps the app alive.
What still bounds it:
- only the desktop's web session can extend it; using the token never does,
and signing out or revoking that session revokes it;
- it lapses 12h after the desktop last asked, and a lapsed row is never
revived: the next launch gets a new token;
- however often it is renewed, a token is replaced after 7 days, and the old
one is revoked on the spot along with the tokens it minted and its sockets.
The desktop no longer re-posts a renewal that kept the same token, so the
app's sockets aren't rebuilt every few hours; an app that reports its token
stopped working is always answered.
* fix: reserve the feedback and no-reply mailbox names again
Signup checks names against `util/reservedUsernames`, but `fbl`, `noreply` and
`no-reply` were added to a second copy of the list in AuthController that
nothing reads any more, so they became claimable. Move them into the live list
and drop the copy.
---------
Co-authored-by: Daniel Salazar <daniel.salazar@puter.com>
|