Commit Graph
2410 Commits
Author SHA1 Message Date
404oops 52f0176854 fix(ai): classify and sanitize upstream credit exhaustion 2026-09-22 21:51:44 +02:00
Daniel Salazar 300fb115a1 feat: opus 5.5 (#3930)
* feat: opus 5.5

* fix: bad typing
2026-09-22 12:31:44 -07:00
dependabot[bot] 70404579f2 chore(deps-dev): bump @types/node from 24.13.4 to 24.13.6 (#3926)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.13.4 to 24.13.6.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.13.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-22 11:33:40 -07:00
Juan Fernando Castro 7462a8eda9 Merge pull request #3914 from HeyPuter/juancastro/put-1869-unhandled-rejection-on-the-hottest-permission-path-can-take
fix: stop leaking an unawaited permission traversal on every check
2026-09-22 14:28:44 -04:00
Daniel Salazar 718de8a9c6 fix: bump server timeouts for better error handling (#3929) 2026-09-22 10:13:46 -07:00
Juan Fernando Castro 476f7e075d Merge pull request #3913 from HeyPuter/juancastro/put-1865-apps-repeatedly-ask-permission-when-it-wants-to-access-files
fix: settle a URL file-access prompt from a grant the app already holds
2026-09-22 12:32:06 -04:00
Daniel Salazar d07ddeaaf5 fix(fs): keep a home directory on its account's username (#3920)
A root was free to be renamed or moved, so it could be parked on a
username no account held yet. The next account to take that name
provisioned a second root at the same path, and from then on either row
could answer a lookup of it — new entries inherit their owner from
whichever row resolved as the parent, so one account's files were
created owned by the other, and renaming the parked root dragged the
other account's subtree along with it.

Rename and move now refuse a root, every username claim checks the home
path before the user row is written, and renameUserHome refuses to heal
onto a path another account holds.
2026-09-21 23:29:09 -07:00
Daniel Salazar 1261976088 feat: user profiles in a system-owned store, public only for paid accounts (#3919)
- ProfileService keeps each profile as /system/profiles/<uuid>.profile,
  admin-owned and served by the protected puter-profiles subdomain
- GET/POST /profile; puter.auth.getProfile/updateProfile, with
  getProfilePicture reading through them
- another user's profile is served only while its owner is on a paid
  plan (profileGate.enabled kill switch); the hosted file is gated
  through the new site.access.check hook the hosting middleware asks
  before streaming any file
- SubdomainStore.create takes isProtected
- the GUI reads and writes the profile through the SDK
- docs: getProfile, updateProfile, UserProfile, limits

PUT-1859 PUT-1860 PUT-1861 PUT-1862
2026-09-21 23:28:56 -07:00
922d203e18 fix(ai): send stable, non-sequential user identifiers to AI providers (#3856)
* fix(ai): send stable, non-sequential user identifiers to AI providers

A precedence bug in the AI providers' identifier expression made every
request send `user: ":undefined"` (the ternary bound the app-uid suffix to
the whole `actor.user.id + actor.app?.uid` sum instead of just the suffix),
or read `actor.user.id` on a missing user. The same expression also shipped
the sequential internal user id, letting AI vendors correlate a single
account across apps and sessions.

All eight OpenAI-, Azure-, xAI-, Meta- and ZAI-style providers now build
the identifier through one shared helper, `aiUserIdentifier()`:

- `puter-<user-uuid>[-<app-token>]`: the random user UUID is always
  preserved in full; `maxLength` constrains only the app-bearing form
- app attribution reads `effectiveApp`, so access-token requests name the
  issuing app instead of looking like direct user traffic
- the app token is truncated to fit the budget, and omitted entirely when
  the remaining budget is below 8 chars, where a truncation could collide
  with another app's uid
- nothing is sent for the system actor
- Meta and ZAI keep a caller-supplied `safety_identifier` / `user_id`
  override, applied before the helper result

`user` is deprecated by OpenAI; the SDK types direct callers to
`safety_identifier` (abuse detection) and `prompt_cache_key` (cache-hit
bucketing). The four OpenAI/Azure chat providers and MetaProvider now send
`prompt_cache_key` as well, defaulting it to the same per-user identifier
unless the caller supplies one; Azure's Grok branch drops both fields,
matching its rejection of unknown args. The cap comment cites only verified
limits: OpenAI's 64 for `safety_identifier` (from the SDK types) and Z.AI's
6-128 for `user_id` (from Z.AI's docs); Meta and xAI document none, so none
is claimed.

The xAI image `#edit` path now carries the identifier like generation, and
takes a named-options param so `user` cannot be transposed with the
adjacent same-typed `aspectRatio`.

Tests share a four-actor matrix (`user` / `user+app` / `access token` /
`system`) with `assertActorMatrixIdentifiers()` across the six
OpenAI-style suites; the helper has exact-string and boundary coverage
(size caps, zero-budget and sub-base cases, no dangling separator, UUID
never truncated, collision guard); the Azure Grok assertions run under a
real user actor so they cannot pass vacuously. 212 provider-suite tests
pass; typecheck and ESLint are clean.

* fix(ai): lock the vendor identifier down and keep vitest out of the test util

Meta and Z.AI no longer let `custom` override the abuse identifier; it is
Puter's attribution, not the caller's. The shared test util exposes pure
field pickers instead of importing vitest into a file the production
tsconfig compiles. The helper's length-cap comment now matches vendor docs
(Meta does cap `safety_identifier` at 64), the redundant budget branch and
the unused export are gone, and the per-user `prompt_cache_key` trade-off is
stated once in the helper instead of five times in providers.

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

---------

Co-authored-by: 404oops <me@404oops.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 16:21:47 -07:00
404oopsandClaude Fable 5.1 027e9a71f3 fix(ai-image): refresh model catalogs, unify txt2img requests, expand docs (#3889)
* fix(ai-image): sync image catalogs, unify request shape, document models

Model sync against every vendor listing and a live generation sweep:
Gemini stable ids replace the retired preview spellings (2.5 Flash Image
delisted ahead of its 2026-10-02 shutdown), OpenAI gpt-image-1/-1-mini/-1.5
are delisted but routable until their shutdown dates with gpt-image-2 as
the default, xAI gains grok-imagine-image-2.0 with its quality tiers,
BytePlus gains the 3K/4K tiers and per-model pixel bounds, Cloudflare
gains SDXL Lightning/Base and SD 1.5 Inpainting, Replicate gains a
schema-driven catalog of 88 additional models with version-pinned
community predictions, and every Together image route is excluded for
the third-party data-sharing requirement.

Request normalization: the driver collapses ratio/width/height/aspect_ratio
into one imageSize with aspect-versus-pixel intent, validates prompt,
quality and resolution once, resolves provider hints (short names or
full driver ids) and aliases with exact ids winning over resellers, and
hands each provider an immutable copy of the caller's args. Providers
share prompt validation, aspect snapping, and content-sniffed data URIs
so responses carry the right MIME type. Replicate predictions are created
once, cancelled on abort or deadline, and bounded in input fan-out.

SDK: txt2img copies caller options, rejects blank prompts with
prompt_required in every call form, and documents that normalize has no
effect because the image return shape is already uniform.

Docs: txt2img options per provider, provider defaults and discovery,
availability notes, a new image model catalog and pricing page, and the
image-generation limits in the quotas page.

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

* fix(ai-image): sniff SVG outputs correctly, reject hex/exponent dimensions

imageDataUri only decoded the first 24 base64 chars (18 bytes) before
sniffImageMime, which cannot see an <svg> root behind an XML prolog -
SVG outputs from recraft-v*-svg models were being labeled image/png.
Decode enough of the payload to cover the 8 KB SVG sniff window.

dimension() accepted string number literals ('0x10', '1e3') as if they
were decimal dimensions; restrict to plain decimal notation.

* docs(ai): link to the model directory instead of a static catalog

The image model catalog and pricing page duplicated the always up to date
model directory at developer.puter.com/ai/models. Link to the directory
from txt2img-related docs and keep the Together data-sharing exclusion
note self-contained.

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 15:00:02 -07:00
404oopsandClaude Fable 5.1 081941a6f3 fix: null-prototype model maps in chat/video drivers; txt2vid prompt and option guards (#3887)
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
Chat and video drivers keyed their provider and model maps on plain objects,
so `model` or `provider` values such as `__proto__` reached Object.prototype
and surfaced as 500s. Both maps are now null-prototype objects and reject
those names as ordinary unknown models.

txt2vid now rejects blank or non-string prompts with `prompt_required` before
any request, tolerates `null` in either argument slot, and copies the caller's
options before resolving the `duration` alias and output path so frozen
option objects work and caller objects are never mutated.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 13:12:35 -07:00
Juan Castro 07e889f135 fix: stop leaking an unawaited permission traversal on every check
`validateUserPerms` started the linked SQL traversal beside the flat read
and returned on a flat hit without awaiting it. The hit is the common
case, so nearly every user-permission check left a floating promise: with
`keep_alive_on_uncaught` unset no `unhandledRejection` listener is
registered, so a DB wobble inside the traversal exits the process.

Run it only when the flat read misses. That also stops a SQL walk the
cache had already answered.
2026-09-21 16:07:26 -04:00
Juan Castro ab7a5f325a fix: refuse an empty app_uid and survive a failed grant check
A present-but-empty `app_uid` fell through to checking the user, where
every `fs:` scope on their own file answers `true` — a prompt settled by
a question nobody answered. It is a 400 now.

The launch-path check also sat outside any catch, so a rejecting read
would have taken the whole launch down instead of falling through to the
prompt. Renamed the seam it is injected through to match the function it
defaults to, since wiring the token-based sibling in its place would send
an app uid as a bearer token and silently always prompt.
2026-09-21 15:56:39 -04:00
Juan Castro b4b464554b fix: settle a URL file-access prompt from a grant the app already holds
Opening `/app/<name>?file=<path>` asked for consent on every launch: the
gate prompted unconditionally and never read the `fs:<uid>:write` grant
its own Allow had written.

`/auth/check-permissions` takes an optional `app_uid` so the account can
ask what one of its apps holds, and the launch gate skips the dialog when
the answer is yes. Sessions only — an app or a scoped token asking would
be a window onto its neighbours' grants.
2026-09-21 15:00:02 -04:00
Juan Fernando Castro a63172e9ae Merge pull request #3892 from HeyPuter/juancastro/put-1806-invite-email-addresses-are-disclosed-to-apps-manage
fix: stop disclosing invite addresses to apps, tokens and delegates

approvals already in place
2026-09-21 12:05:08 -04:00
Juan Castro 72203152d9 fix: bound a scoped token to what it issued, which is nothing
CI caught three HTTP-level tests the earlier merge left behind, and they
were right to fail: dropping this branch's `manage` gate in favour of
main's row filter lost a case main's filter does not cover.

Main bounds an app to the rows it issued. A *scoped* access token is not
an app, so it was falling through unbounded — `/fs/stat` with
`return_shares` handed a `fs:<uid>:list` token the owner's whole share
list. Addresses stayed withheld, but who else can reach a file is no
more a narrow token's business than the addresses are.

So the filter is generalised rather than the gate restored: a scoped
token is bounded to nothing, since it issues nothing under its own name.
Filtering it by a null app would have been worse than not filtering —
that matches the owner's own rows. Apps and sessions behave exactly as
they do on main, and a full-access token still holds the account's reach.

The three tests now assert the answer instead of a refusal, including
the one whose name had always promised a refusal its body never checked.
2026-09-21 11:12:37 -04:00
Juan Castro cec8382d4d Merge branch 'main' into juancastro/put-1806-invite-email-addresses-are-disclosed-to-apps-manage
Two textual conflicts, both additive on each side: `isAccountContext`
(here) and `isPlainUserActor` (main) are both imported and both used,
and the `stat()` note keeps both sentences — this branch's on who may
read an invite address, main's on the share-read limit it spends.

The rest is adapting to main, which grew its own answer to half of what
this branch was for. `listSharesOf` there bounds an app to the rows it
issued itself; this branch instead required the credential to hold
`manage` and refused it otherwise. Main's is the better mechanism — it
answers the app rather than turning it away, and it hides other
issuers' rows outright rather than redacting a field on them — so the
`manage` gate goes, and with it the two tests that asserted the
refusal. They are replaced by tests that hold main's line: an app sees
none of the invites it did not send, with or without `manage`.

What this branch still carries is the gap main does not close. Its row
filter only applies to apps, so a plain manage delegate still reads the
owner's invite addresses; `#maySeeInviteAddress` is what withholds
those, and its delegate tests pass unchanged. The `stat()` note is
corrected to describe main's behaviour rather than the removed gate.
2026-09-21 10:46:07 -04:00
Juan Castro 3f335939b3 fix: team shares are not subject to a recipient's per-sender block
Review call from the ticket's author: a team share is the team's, not
one colleague's to withhold from another, so a personal block should not
hide it. This drops the enforcement added earlier on the branch — the
listing, its total and the fs-event fan-out no longer filter team rows
by the recipient's block list, and the SQL fragment that did it goes
with them.

What a block still does for a team share is suppress the notification,
which was already true before this branch: it stops the interruption
without pretending to stop the access. Leaving the team is what ends
that. Said so in the block API docs, the settings copy and the code,
since the mismatch between the two was the original complaint.

The unshare sweep is untouched — that half of the ticket stands.
2026-09-21 10:28:50 -04:00
Juan Castro 4e821c128c Merge remote-tracking branch 'origin/main' into juancastro/put-1813-team-share-blocklist-and-unshare-paging
# Conflicts:
#	src/backend/services/share/ShareService.ts
#	src/backend/stores/share/ShareStore.js
2026-09-21 10:28:33 -04:00
Juan Fernando Castro 76e0d2ec93 Merge pull request #3871 from HeyPuter/juancastro/put-1798-sharing-email-notification-changes
feat: name the issuing app in share emails, and cut the digest window to 5s
2026-09-21 09:58:09 -04:00
Juan Fernando Castro 24fb679dd3 fix: charge stat's return_shares against the share-listing budget (#3870)
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: charge stat's return_shares against the share-listing budget

`return_shares` on /fs/stat and the legacy /stat runs the same work as
GET /share/shares, but was only metered under fs:stat's far more
generous limit — and the two scopes stacked instead of sharing one
counter.

Adds consumeRouteRateLimit(req, spec), an imperative charge that
resolves the key and per-subscription limit exactly as rateLimitGate
does, so a handler can conditionally spend a second scope when a
request flag makes the route expensive. Both stat handlers now charge
share:list before doing the listing work; SHARE_LIST_LIMIT moves to a
shared share/limits.ts so all callers pin the same spec.

Closes PUT-1597.

* feat: consumeRouteRateLimit takes the array spec form too

Review follow-up on #3870: a multi-window spec passed whole would have
read an undefined window and silently never pruned. Charge each window
in order instead, refusing on the first refusal, matching the gate.
2026-09-19 14:11:29 -07:00
Daniel Salazar eb9f03e984 fix: misc hardening + other fixes (#3906) 2026-09-19 12:57:57 -07:00
Daniel Salazar 9292771554 fix: hardening (#3904)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
2026-09-18 11:22:39 -07:00
Daniel Salazar b39bb771d9 feat: sortby for recursive readdir (#3900) 2026-09-17 17:29:55 -07:00
Daniel Salazar 0be3bc55c2 feat(metering): AI cost multiplier hook for AI drivers (#3898)
Emits ai.cost.multiplier.<driver>.<provider>:<model> before recording AI
usage, so what a model costs to charge is policy an extension owns rather
than a number in core. Nothing listening records the provider cost.

MeteringService.withAiCostMultiplier(driver) returns a view of the service
whose recording paths scale costOverride by the hook's answer; every AI
driver hands that view to its providers, so all of them are covered without
touching provider code.
2026-09-17 15:18:38 -07:00
Daniel Salazar 51ce868d1e fix: shared kv handles also allow value opt in (#3895)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
2026-09-17 10:34:35 -07:00
Juan Castro 461cb7d0a9 fix: close two gaps the review found in the invite-address fix
- A full-access token was denied the address while `shared-by-me` still
  handed it the same rows, so the clause bought no privacy and cost an
  API client the address `unshare()` takes. `isAccountContext` is the
  boundary the rest of the codebase already uses for 'acting as the
  account': plain session or full-access token, never a scoped one.
- The Dashboard share modal dropped a withheld invite entirely, since
  its aggregate keys a pending row on the address — so a delegate saw no
  sign of an outstanding invite and accessCount under-reported who could
  reach the item. It is now kept, keyed on the share uid, labelled, and
  without the controls that would need a recipient to address.
2026-09-17 13:21:13 -04:00
Daniel Salazar bb97660ad7 feat: kv events value opt in (#3894)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
2026-09-17 00:17:12 -07:00
Juan Castro 3e3d02bafe fix: stop disclosing invite addresses to apps, tokens and delegates
getShares (and stat's return_shares, which runs the same listing) gated
on #assertCanManage's default 'see' mode, so any credential that could
see the node got every unclaimed invite's raw email — including an app
handed one file by the picker, a list-scoped token on the stat surface,
and a manage delegate reading the owner's invitees.

Two bounds, matching the invariant clientShare.ts already claimed:

- An invite's address goes only to the item's owner and to whoever sent
  it, and never to an app or token. A delegate can revoke only what they
  issued, so withholding costs them nothing they could act on.
- An app or token must hold manage reach of its own to read the listing
  at all; it answers for the ancestors too, which is not what being
  handed one file grants. tryListSharesOf turns that into an empty
  shares array, so stat itself keeps working.

The share dialog names an unattributable invite rather than rendering a
blank row with a dead revoke button.

Closes PUT-1806.
2026-09-16 18:59:10 -04:00
Juan Castro 728966bfb1 Merge branch 'main' into juancastro/put-1813-team-share-blocklist-and-unshare-paging 2026-09-16 18:05:58 -04:00
Juan Castro 3f7ee34706 Merge branch 'main' into juancastro/put-1798-sharing-email-notification-changes 2026-09-16 18:05:39 -04:00
Daniel Salazar 194ca7c789 fix: list a link share only while the owner's plan covers it (#3891)
* fix: list a link share only while the owner's plan covers it

* chore: types + jsdoc cleanup
2026-09-16 14:42:52 -07:00
Juan Castro 23c33b2e06 Merge remote-tracking branch 'origin/main' into juancastro/put-1813-team-share-blocklist-and-unshare-paging
# Conflicts:
#	src/backend/stores/share/ShareStore.js
2026-09-16 15:27:09 -04:00
Daniel Salazar 1b6334c02c feat: a paid plan counts as a verified card for the sharing gate (#3886)
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: list a link share only while the owner's plan covers it

* fix: fs limits for signed urls

* feat: a paid plan counts as a verified card for the sharing gate
2026-09-16 10:18:47 -07:00
Juan Castro 0593844acf Merge branch 'main' into juancastro/put-1813-team-share-blocklist-and-unshare-paging 2026-09-16 13:05:03 -04:00
Juan Castro 67d048148c Merge branch 'main' into juancastro/put-1798-sharing-email-notification-changes 2026-09-16 13:04:43 -04:00
Juan Castro 5c8f02384c test: schema version is 80 since 0084
#3874 added 0084_share-anyone-with-link.sql, taking the sqlite chain's
target user_version to 80, but CURRENT_SCHEMA_VERSION stayed at 79 —
and the test workflow only runs on PRs, so main broke silently and every
open PR's 'test (base)' job now fails on it.
2026-09-16 12:48:07 -04:00
Juan Castro 5d20e054ac Merge branch 'main' into juancastro/put-1813-team-share-blocklist-and-unshare-paging 2026-09-16 10:33:03 -04:00
Daniel Salazar 59f73528cb feat: share with anyone with the link (PUT-1580) (#3874)
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
* feat(email): inline cid attachments and Puter mailbox delivery for sendTransactional

EmailAttachment gains cid/contentDisposition so the transactional driver can send inline images. The SDK's EmailAttachment typedef now comes from types.js, which already carried cid. Docs describe delivery to <username>@puter.email recipients and the not_found code.

* feat: share with anyone with the link (PUT-1580); gate sharing on a verified phone or card
2026-09-15 21:06:12 -07:00
Juan Castro 27e858d760 fix: gate the group-share index delete on authority, qualify the live filter
Review follow-ups on #3872: deleteActiveGroup ran even when every
canManagePermission check was denied, deleting the index row while the
team's grant survived — unlisted live access, the class this PR exists
to remove. And memberIdsAmong's join left deleted_at unqualified, which
only works while jct_user_group has no such column; #live now takes an
alias.
2026-09-15 18:57:50 -04:00
Juan Castro 0bd6458441 test: pin the mixed-digest button link carrying no app
Review follow-up on #3871: the flush-level rule — shared_app on the
button only when every entry came through one app — had no direct
coverage. An app share and a plain share folded into one digest now
prove the line and item link keep their attribution while the button
stays clean.
2026-09-15 18:56:38 -04:00
Daniel Salazar 45aff36f0c fix: auto create folders for fs perms (#3873)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
2026-09-15 15:11:13 -07:00
Juan Castro af6547724d Merge branch 'main' into juancastro/put-1813-team-share-blocklist-and-unshare-paging 2026-09-15 15:23:22 -04:00
Juan Castro e9922aea26 Merge branch 'main' into juancastro/put-1798-sharing-email-notification-changes 2026-09-15 15:23:11 -04:00
Juan Fernando Castro b136c56cb5 Merge pull request #3846 from HeyPuter/juancastro/put-1792-optional-seat-email
feat: the team seat experience — no email required, forced password change, team label, and plan-based limits (PUT-1792)

Note: Bypassing the code owners rule, since there are couple approvals in place for this.
2026-09-15 14:53:06 -04:00
Juan Castro aadcda073d fix: keep blocks out of the authority graph, and sweep what unshare missed
Code review on the previous commit confirmed three ways the block filter
inside readUserGroupPerms turned a reversible block into permanent loss:
canManagePermission reads the same rows, so a block-suspended grant
looked revoked to #revokeDownstream's cascade, to unshare's authority
gate, and to invite claiming — deleting re-shares and invites that were
supposed to come back on unblock. The scan is now deliberately blind to
blocks; a block suspends delivery only (listing, count, fan-out,
telling), which the share store filters itself through one shared
notBlockedSql fragment owned by UserBlockStore.

The team-unshare sweep also now covers what it claimed to: a member's
re-share *to another team* is revoked with them (group rows swept in
 #revokeDownstream, user path included), and the sweep is skipped
entirely while another issuer's grant still backs the team — members'
re-shares rest on that authority and revoking them would orphan live
access. The blanket block-all note in settings now says team shares
still arrive, matching what the code deliberately does.
2026-09-15 14:49:54 -04:00
Juan Castro 9da03555ae fix: make the block list bite on team shares, and unshare every re-share (PUT-1813)
A sender a member blocked could still land items in that member's
shared-with-me — and grant them real access — by sharing with a common
team. The group grant is one row and cannot exclude a member, so the
block is now enforced where delivery is derived per member: the
permission scan, the inbound listing and its count, and the fs-event
fan-out all skip team rows whose issuer the member blocked. Nothing is
revoked, so unblocking restores everything. Blocking now also bumps the
blocker's permission cache so it bites immediately. Direct shares keep
their contract: existing ones stand.

#unshareTeam swept re-shares by listing members with a 200 cap and
never following the cursor, so members past the cap kept their orphaned
rows. It now walks the share rows in the subtree — the set that can
actually need sweeping — and filters those issuers to members, which
has no cap by construction.
2026-09-15 14:49:54 -04:00
Juan Castro f428797a79 feat: render the teams UI for allowlisted users without the global switch
`gui_params.teams_ui` stays the deployment-wide switch; with it off, the
tab now also renders for a signed-in user who passes the email-domain
allowlist (membership included, so seats see their roster). Anonymous
renders hide it, and the API keeps deciding real access either way.
2026-09-15 14:48:11 -04:00
Juan Castro ad3d15e7e2 feat: stage the teams rollout behind an email-domain allowlist
`teams_allowed_email_domains` limits who may enter the teams surface;
unset keeps today's behavior. Gated on the two routes that constitute
entry — creating a team, and the listing that shows the tab — with the
same 404 a teams-off deployment answers, so the GUI needs no change and
a staged rollout is indistinguishable from the feature being off.
Members of an existing team always pass, whatever their domain: an
allowed owner brought them in, and the surface follows the team.
2026-09-15 14:23:49 -04:00
Daniel Salazar 2fdd67c72e fix: tighten up fs perm strings (#3860) 2026-09-15 10:33:47 -07:00