Commit Graph
5773 Commits
Author SHA1 Message Date
404oops 909d87560f fix(realtime): evict revoked credentials and recheck paid eligibility 2026-11-01 19:25:58 +01:00
404oops 7f91e35b7d fix(realtime): bound upgrades before credential admission 2026-11-01 19:25:52 +01:00
404oops db310b1bc2 fix(convo): reject interrupt readiness failures asynchronously 2026-11-01 19:25:43 +01:00
404oops 6ed7e8b452 test(realtime): isolate credential attempt budgets between cases 2026-11-01 19:25:40 +01:00
404oops 9decce5f0b fix(realtime): redact structured provider diagnostics 2026-11-01 14:54:32 +01:00
404oopsandClaude Opus 5.5 60c446fb48 fix(metering): type the array read's retry callback
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:50:48 +01:00
404oopsandClaude Opus 5.5 692a20d869 fix(metering): keep buffered and store reads from missing or doubling usage
Three reads still got a flush wrong:

- A write re-sent after its reply was lost, found already applied, left
  the claim without touching a warm cached base, so balance reads stayed
  short of what the store held. The claim and the base are now dropped
  together, and the next read seeds a fresh base from the store.
- A read with no cached base took the buffer, then the store; a flush
  that wrote a batch and left its claim in between had it counted twice.
- An exact read added another deployment's claim but not the usage that
  arrived behind it, which that read can't flush, so a charge crossing
  the allowance went entirely to the allowance.

Reads that go to the store now take everything buffered (delta and
claim), read the store consistently, and read the buffer again, retrying
when it shrank in between. Single and array reads and exact reads all
use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:49:36 +01:00
404oops 20cd119fb1 Merge branch 'fix/convo-mic-reconnect' into fix/metering-buffer-flush-reads 2026-10-31 17:45:12 +01:00
404oops 13b1292b7a Merge branch 'fix/convo-server-message-limit' into fix/convo-mic-reconnect 2026-10-31 17:45:12 +01:00
404oops 8d9298a97d Merge branch 'fix/realtime-credit-admission' into fix/convo-server-message-limit 2026-10-31 17:45:11 +01:00
404oops 8900bf033e Merge branch 'fix/realtime-transient-failures' into fix/realtime-credit-admission 2026-10-31 17:45:11 +01:00
404oopsandClaude Opus 5.5 280fdb38c2 fix(realtime): judge a refusal by the plan it was decided on
A plan or funds refusal was classified by a plan lookup made after the
decision. A fallback plan is cached for only a few seconds, so when it
ran out in between, the refusal was judged against the recovered plan
and still ended the session with a terminal 1008. The plan is now also
checked just before each decision, and a refusal is retryable if either
check found a fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:45:05 +01:00
404oopsandClaude Opus 5.5 7049eb4481 fix(metering): read the claim before the store in an exact read
`readExact` read the store and the outstanding claim side by side. When
a flush wrote a batch and took it off its claim between the two, the
store read had the counter from before the write and the claim read had
nothing, so the batch was missed: 10 units recorded, 0 read.

The claim is now read first. An amount gone from it by then was written
before the store read, which is consistent, so the read has it. When the
claim changes during the store read, the batch may be in both, so the
read is retried, up to three times.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:31:48 +01:00
404oops 880bb4eea1 Merge branch 'fix/convo-server-message-limit' into fix/convo-mic-reconnect 2026-10-31 17:08:18 +01:00
404oops 3bf19f51bd Merge branch 'fix/convo-mic-reconnect' into fix/metering-buffer-flush-reads 2026-10-31 17:08:18 +01:00
404oops 90ca77f3ec Merge branch 'fix/realtime-credit-admission' into fix/convo-server-message-limit 2026-10-31 17:08:17 +01:00
404oops 6a9db3a9db Merge branch 'fix/realtime-transient-failures' into fix/realtime-credit-admission 2026-10-31 17:08:17 +01:00
404oopsandClaude Opus 5.5 7a723c5201 test(puter-js): fail credential lookups until the relay hits one
The reconnect test made the next credential lookup fail once, but the
SDK's own socket.io connection could take that failure before the
relay's reconnect attempt did, so the relay never saw a failed lookup
and the test failed in CI. Lookups now fail until the relay has refused
an attempt on one, then recover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:08:17 +01:00
404oopsandClaude Opus 5.5 a8f7c94f2e fix(metering): count claims on the write path and read missing bases exactly
The running total `incr` returns and `readExact` also missed usage a
flush had claimed but not yet written. Both feed how a charge is split
between the allowance and purchased credit, so an account could be
charged to its allowance for usage that had already used it up. Both
now add the counter's outstanding claim.

A read or seed with no cached base asked the store with an ordinary
read, which could miss a write a settle had just made; it now reads
consistently. A write-through after a cache failure drops the cached
base, which doesn't have the write, so the next read reseeds it.

The dirty-set reconcile test no longer depends on how many flush cycles
earlier tests ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:56:18 +01:00
404oops e39e480c9f Merge branch 'fix/convo-server-message-limit' into fix/convo-mic-reconnect 2026-10-31 16:50:07 +01:00
404oops 3685e45866 Merge branch 'fix/realtime-credit-admission' into fix/convo-server-message-limit 2026-10-31 16:50:07 +01:00
404oops 4b1a3461fd Merge branch 'fix/convo-mic-reconnect' into fix/metering-buffer-flush-reads 2026-10-31 16:50:07 +01:00
404oopsandClaude Opus 5.5 ae452aeaad perf(metering): stop releases from churning the reservation version
Every release bumped the balance version, so an account's own short-lived
holds (each realtime session probes one and releases it at once) could
keep its other reservations reading a stale version until they were
refused. The usage a hold stands for already bumps the version when it's
recorded, before the hold is released, and a release without usage
leaves the balance as it was, so releases no longer bump it.

The admission read also uses the cached add-ons record when it shows no
purchased credit left, which can only understate the balance; it reads
consistently only while credit remains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:50:02 +01:00
404oops 85e3a19ed6 Merge branch 'fix/realtime-transient-failures' into fix/realtime-credit-admission 2026-10-31 16:48:17 +01:00
404oopsandClaude Opus 5.5 b33a45732f fix(realtime): retry instead of refusing when the plan lookup fails
When the billing lookup failed, metering answered with a provisional
fallback plan, so the relay refused the session with
`subscription_required` or, against the fallback's allowance,
`insufficient_funds`, both terminal 1008 closes, and the SDK showed the
upgrade prompt to a paying account. A plan or funds refusal decided on a
provisional plan now sends `internal_error` and closes with 1011, at
connection and mid-session, so the client retries once the lookup
recovers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:48:11 +01:00
404oopsandClaude Opus 5.5 a64dcfe322 fix(metering): count usage mid-flush in balance reads
A flush takes a counter's buffered amounts aside before writing them
onward, and reads only looked at the live delta and the cached base.
From the claim until the settle replaced the base, those amounts were
missing from every read: one store write normally, up to the orphan
sweep if the write failed. Balance checks, credit reservations among
them, could see an account as having more than it does.

Reads now add the counter's outstanding claim. A batch written onward
leaves the claim and joins the cached base in one step, so it stays
counted until the settle replaces the base with the store's view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:58:50 +01:00
404oopsandClaude Opus 5.5 8416d9738d test(puter-js): prove microphone chunks hit the closing-socket gap
The browser test now counts the chunks the transport refused while the
socket read as closing, and requires at least one, so it can't pass
without exercising the gap. The socket stays closing until the
connection drops instead of going back to open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:53:00 +01:00
404oops edaf4c185d Merge branch 'fix/convo-server-message-limit' into fix/convo-mic-reconnect 2026-10-31 15:51:52 +01:00
404oopsandClaude Opus 5.5 88636b173b fix(puter-js): end the conversation on an oversized event mid-reconnect
A server event over the 4 MiB limit closes the socket with 4009, which
ends a connected conversation. During a reconnect attempt the same close
was reported as `session_closed`, which is retried, so the conversation
tried ten more times and finally closed with the original drop code.
A replacement session opened after a restart notice was reopened again
and again for the same reason.

A 4009 close now ends the conversation during a reconnect too, reporting
4009, and a replacement session that hits it isn't reopened. Tests cover
both, a multibyte event of exactly 4 MiB, and a whole retrieved item
through the relay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:51:51 +01:00
404oops 93a5083190 Merge branch 'fix/realtime-credit-admission' into fix/convo-server-message-limit 2026-10-31 15:49:40 +01:00
404oopsandClaude Opus 5.5 347bc77984 fix(metering): refuse a reservation whose balance never holds still
A reservation that found the balance changed three times in a row used
to skip the version check on its fourth attempt, which brought back the
race the check closes; an account could provoke that itself. It now
waits a short randomized time between attempts and refuses after five.

A missing version key now starts from a random value, so a version read
before the key expired can't match its successor. Credit grants,
allowance changes, manual usage corrections and a usage write that
fails partway also bump the version, and the store refuses an amount
it can't hold instead of storing a negative hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:49:35 +01:00
404oops c4e6b628c2 Merge branch 'fix/realtime-transient-failures' into fix/realtime-credit-admission 2026-10-31 15:47:50 +01:00
404oopsandClaude Opus 5.5 6049ba2741 test(realtime): cover a revoked session credential
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:47:48 +01:00
404oopsandClaude Opus 5.5 a607c347dc fix(puter-js): keep microphone capture when the socket closes first
A conversation still reads as connected for a moment after its socket
starts closing. A microphone chunk sent in that moment got
`session_closed`, and the capture handler treated that like a real
media failure: it stopped the tracks and the AudioContext, so the
replacement session after the reconnect had no microphone.

The built-in microphone now drops a chunk the transport refuses with
`session_closed` or `session_not_ready`, as it already did for
`queue_full`, and keeps capturing. A reconnect resumes sending; a final
close still stops capture through the conversation, and processor or
other errors still release it. Dropped chunks aren't replayed, and
public sends keep reporting the transport error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:17:10 +01:00
404oopsandClaude Opus 5.5 c66deb30f5 fix(puter-js): accept provider events up to the relay's 4 MiB cap
The conversation transport refused any server event over 64 KiB with a
terminal 4009 close, the limit meant for what the SDK sends. The relay
forwards provider events up to 4 MiB, and a session.updated echo or a
whole audio item easily passes 64 KiB, so valid conversations ended.

Incoming events now have their own 4 MiB limit, matching the relay,
counted in UTF-8 bytes (a string short enough that it can't exceed the
limit isn't encoded to check). An event over it still closes with 4009
and doesn't reconnect. Uploads keep their 64 KiB cap, and malformed
events are still ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:17:10 +01:00
404oopsandClaude Opus 5.5 ad87892cd2 fix(metering): reserve AI credit only when it fits, in one step
`reserveAiCredits` checked the balance and then took an unconditional
hold, so concurrent requests could all pass the check before any hold
was visible; two 16.2M µ¢ holds were admitted against a 20M µ¢ balance.

The hold store now takes a hold only if it fits beside the live holds,
in one Lua script on the actor's hold key. A per-user version, bumped
when usage is recorded and when a hold is released, lets the script
refuse a balance read that a settlement has since overtaken; the
service reads the balance again and retries, and skips the version
check on its last attempt rather than refuse an account whose balance
keeps moving. Both keys share the actor's hash tag, so the script runs
on Redis Cluster. The balance read for this check reads purchased
credit consistently.

`reserveCreditsIfAffordable` carries this through the service, and the
AI facade calls it once with the cost factor applied. `reserveCredits`
keeps its unconditional semantics. When the cache is unreachable, the
balance alone decides and nothing is held, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:17:10 +01:00
404oopsandClaude Opus 5.5 4c308ef1f0 fix(realtime): tell transient server failures apart from bad credentials
A credential lookup that threw (a database or cache outage) was treated
as no actor, so the session got `invalid_api_key` and a 1008 close. That
is terminal for the SDK, which then ended an established conversation
that only needed to reconnect. A failed lookup now gets `internal_error`
and closes with 1011, which the SDK retries; missing, invalid and
revoked credentials keep `invalid_api_key` and 1008. Nothing is cached
for the failed lookup, and the cause is logged server-side only.

A session whose usage records pile up behind metering now closes with
1013 instead of 1008, still with `rate_limit_exceeded`. Queued usage is
still recorded and its holds released. An unexpected setup failure now
sends `internal_error` before its 1011 close.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:17:10 +01:00
404oopsandClaude Opus 5.5 62aebe2cf9 fix(realtime): listen for client socket errors before refusing a session
A session refused while the server drains returned before its client
error and close listeners were registered, so a frame that errored the
socket during the close handshake (an oversized one, for example)
emitted an unhandled `error` and ended the process. The listeners now
go on as soon as the session exists, ahead of every refusal.

The 404 answer to an upgrade for another host also ends a raw socket
that has no error listener once Node hands it over; it now has one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:32:06 +01:00
Daniel Salazar 52aedde3c5 fix(kv): KV share authz and load hardening (#4430)
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(permissions): delete user-to-user grants by their exact spelling

MySQL compares the permission column case-insensitively, and LIKE folds
case on MySQL and SQLite, so withdrawing a grant could also remove one
spelled differently, which checks and the flat view treat as distinct.
The single, batch and subtree deletes for one holder now match the
permission byte for byte.

* fix(auth): refuse key-value share grants on /auth/revoke-user-user

Withdrawing a share's grant there left its handle live and listed. The
route now refuses kv-share permissions and points to puter.kv.unshare().

* perf(kv): ask whether a delegate app is authorized once per check

The app-delegate implicator re-read the app's authorization for every
option a refused check offered, one read per ancestor of a deep key.

* fix(puter-js): default a kvShare consent request to read

puter.kv.share() creates read shares by default, so a consent asked for
with the defaults (watch) didn't cover the share made with the defaults.

* fix(kv): treat a private entry as missing for a share holder's writes

Through a handle, a private entry reads as absent, but writes to it were
refused with an error naming it as private, and onlyIfAbsent answered
false. Deleting one through a handle now answers as deleting a missing
key does and leaves the entry in place. Any other write is refused with
kv_handle_access_denied, the same error as a call the share's level
doesn't allow, onlyIfAbsent included. Cross-app access is unchanged.

* fix(kv): cap handle traffic per owner namespace

Every call through a share handle lands on its owner's namespace, and
per-holder limits put no bound on what all holders together could send
there. Handle calls into one namespace now share a budget of 6,000 units
per 10 seconds, counted from the storage reads and writes each call
consumes (at least one), and are refused with too_many_requests past it.
2026-10-31 01:01:53 -07:00
Daniel Salazar 7f832ede43 fix(events): KV share handle subscription hardening (#4428)
* fix(events): keep a share's prefix out of handle subscribe errors and limits

A `kv:<handle>:<key>` pattern is now checked against the 256-character
limit as the holder wrote it, before it's composed onto the shared
prefix. The composed pattern is compiled with room for the longest
prefix a grant can carry, at subscribe and at dispatch, so neither the
limit nor its error depends on the prefix.

A handle subscription's `match` is read off the holder's own subject
instead of the stored composed pattern, so whether it's `null` no
longer depends on how deep or long the prefix is.

* fix(events): cap and end session subscriptions made through a share handle

Session subscriptions made through a share handle are indexed under the
handle in the owner's keyspace. A holder may keep 100 of them through
one handle across all their connections; past that, subscribe fails
with `events_subscription_limit`. The owner's own subscriptions through
the handle don't count.

Retiring a handle now ends its session subscriptions in the same
region and sends `events.ended` with `permission_revoked`, alongside
the existing durable settle. A socket refresh no longer renews a row
whose handle was retired or whose grant is gone: it ends it the same
way, which is how a retire made elsewhere reaches these rows.

* fix(kv): read a forgotten share handle from the primary

After a handle or its owner is forgotten, reads of that handle go to
the primary for the next minute, so a replica that hasn't caught up
can't put the old row back in the cache. A read that a forget lands on
while it's in flight is answered but not cached.

* fix(events): read a socket ref's subId after the last `|`

A KV token carries the subscribed key, which can contain `|`, so
splitting the ref at the second one cut the token short. A subscription
on such a key couldn't be unsubscribed, and a reap left its row behind
in the owner's keyspace. SubIds never contain `|`, so the ref is split
at the last one.
2026-10-31 00:58:42 -07:00
Daniel Salazar 11114c80d6 fix(auth): fail closed on otp-login token reuse check (#4421)
/login/otp and /login/recovery-code marked the otp-login token used
through a rate-limit check that admits on a store error, so while the
store was unavailable a captured token could complete another sign-in.
The check now fails closed.

Also drops the check-translations scripts and the AGENTS.md step that
runs them; the script they call was removed in #2873.
2026-10-31 00:26:56 -07:00
Daniel Salazar 5d907d18c9 fix(socket): bound acks a receipt ask leaves on sockets that never answer (#4422)
An acked broadcast leaves its callback in socket.io's per-socket ack map on every socket it reaches, and nodes other than the asker free it only when that socket answers or disconnects. Each socket's map is now a BoundedTtlMap capped at 64 entries and 60 s, so a socket that never answers holds a bounded number and a late answer is ignored. BoundedTtlMap drops expired entries on set so its TTL frees memory too.
2026-10-31 00:26:44 -07:00
Daniel Salazar 2d5e037599 fix(ai): bill realtime responses the upstream drops before reporting usage (#4423)
When the upstream realtime connection closes, for any reason, with a
started response that never sent response.done, the relay now records that
response at its credit hold (8k audio in + 4,096 audio out at the session
model's rates, through the AI cost factor) instead of releasing the hold
uncharged. The hold is released once the charge is recorded, and a warning
alarm is still raised.

Client conversation.item.retrieve events and MCP tools in session.update or
response.create are refused with an invalid_event error and a 1008 close.
2026-10-30 23:48:58 -07:00
8d3a1ccc2b feat(notifications): remove a notification from the dashboard history (#4412)
Each notification in the dashboard list gets a remove (✕) control next to
mark-as-read. It calls the new POST /notif/delete, which deletes the user's
own row and sends `notif.removed` to their other tabs so the entry and any
toast for it disappear everywhere.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Daniel Salazar <daniel.salazar@puter.com>
2026-10-30 22:52:27 -07:00
Daniel Salazar a1fe14cbd5 fix(ai): bound large AI request bodies per user and to chat (#4420)
Follow-up to #4408.

The 503 returned when a process holds too many large bodies is load
shedding, so it records an info alarm instead of a critical one.

One user can hold at most 128 MiB of large bodies at once; past that the
request gets 429 with Retry-After, so a single account can't take a
process's whole budget, e.g. with slow uploads.

Through /drivers/call only puter-chat-completion takes a body over 50 MiB;
other drivers keep the default limit and get 413. The check reads the
parsed body's interface, since route matching ignores case and trailing
slashes.
2026-10-30 21:16:53 -07:00
Filip KujundžićandDaniel Salazar a195605003 fix(ai): accept 128 MiB Anthropic message requests (#4408)
* fix(ai): accept larger Anthropic message requests

* fix(ai): accept 128 MiB bodies on /drivers/call and OpenAI routes

Extend the 128 MiB JSON limit to /drivers/call (puter.ai.*) and the
OpenAI-compatible routes, alongside the Anthropic ones.

A body over the default 50 MiB is read only after a header or cookie
credential resolves; otherwise the global parser rejects it with 413, so
unauthenticated callers can't make the server buffer and parse 128 MiB.
Large bodies skip the rawBody copy, and a per-process budget caps how many
bytes of them are held at once, returning 503 with Retry-After past it.

The prompt token estimate no longer splits text on whitespace, which took
seconds and allocated a string per word on large prompts. It counts
whitespace runs in a loop, scans at most 1 MiB per prompt and extrapolates
the measured density past that.

---------

Co-authored-by: Daniel Salazar <daniel.salazar@puter.com>
2026-10-30 20:52:00 -07:00
Daniel Salazar 1e39936d12 fix: custom domains (#4419) 2026-10-30 20:39:11 -07:00
dependabot[bot] 6d47db9358 chore(deps): bump nodemailer from 9.1.1 to 10.1.4 (#4386)
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
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 9.1.1 to 10.1.4.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v9.1.1...v10.1.4)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 10.1.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-30 19:39:17 -07:00
spacedandDevin AI 7ac2f2e247 feat(ai-chat): let requests opt out of provider fallback (#4343)
* ai-chat: fallback: false fails on the first route instead of rerouting

A chat request whose first provider fails is retried on up to two other
providers that serve the same model, and the caller pays for whichever
one answers. Callers who want one specific route, or who want to handle
a vendor failure themselves, had no way to turn that off.

Passing fallback: false (puter.js option, /drivers/call arg, and the
OpenAI-compat and Anthropic-compat request bodies) makes the first
attempt's failure final: the route is still marked unhealthy, and the
error is classified the same way an exhausted chain would be.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* ai-chat: cover fallback: false in the SDK suite

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* ai-chat: fallback: false rejects a pinned provider with no route for the model

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* docs: plainer wording for the fallback option

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-30 19:35:45 -07:00
Juan Fernando Castro fea89c91b8 feat(puter-js)!: remove notifications.registerDevice (#4409)
* docs(notifications): registerDevice takes ios or android

* feat(puter-js)!: remove notifications.registerDevice
2026-10-30 19:31:30 -07:00