Cloud Team Collections (S3 + Supabase backend)#8048
Conversation
Design for replacing the Dropbox-folder Team Collections backend with a
transactional S3 + Supabase backend ("Cloud Team Collections"):
- Design/CloudTeamCollections.md: requirements and decisions (approved-accounts
sharing, Send/Receive modal model, versioned-mirror persistence with no
content retention, .bloomSource Lost & Found recovery with server-side
incident events, coexistence with folder TCs, no auto-migration in v1),
architecture, base-class seams, UI changes, test plan, roadmap.
- Design/CloudTeamCollections/CONTRACTS.md: frozen v1 API contracts (RPCs,
edge functions with exact request/response shapes, link-file format,
S3 layout, realtime message shape).
- Design/CloudTeamCollections/IMPLEMENTATION.md: master checklist - five
build waves, branching strategy, shared-file schedule, merge protocol.
- Design/CloudTeamCollections/tasks/00..10: per-task child plans (goal,
dependencies, file ownership, steps, acceptance criteria).
Full interactive review document (diagrams, ~135-case edge-case matrix,
auth decision brief): https://claude.ai/code/artifact/14157962-d8e4-4c3f-afa7-34c49ed29e1a
Design only - no product code changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All development and testing now targets a fully local stack (local Supabase via CLI, MinIO as a filesystem-backed S3 substitute, local GoTrue email/password auth that accepts any login) so implementation can start before the real S3 bucket, hosted Supabase, or Firebase/BloomLibrary auth changes exist. Adds task 11 (local-dev-stack), makes the auth Option A/B/C decision non-blocking, and tracks real-infrastructure cutover as a deferred config-swap list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ms, capability flags, feature flag) - New TeamCollectionLink.cs: parses/writes folder-path and cloud://sil.bloom/collection/<id> forms of TeamCollectionLink.txt; throws InvalidTeamCollectionLinkException on bad content - TeamCollectionManager: replace 3x hardcoded `new FolderTeamCollection(...)` with a CreateTeamCollectionFromLink factory; add ConnectToCloudCollection (throws NotImplemented); add ConnectToCloudCollection to ITeamCollectionManager interface - TeamCollection: add TryLockInRepo/UnlockInRepo virtual seams; route AttemptLock, UnlockBook, ForceUnlock through them; folder default keeps exact read-modify-write behavior - TeamCollection: add SupportsVersionHistory, SupportsSharingUi, RequiresSignIn capability flags (all false on the folder backend) - ExperimentalFeatures: add kCloudTeamCollections token - FeatureRegistry: add CloudTeamCollection feature entry (LocalCommunity tier, gated behind kCloudTeamCollections experimental flag) - New TeamCollectionLinkTests: 24 tests covering parse/write round-trips, folder form, cloud form, garbage content, missing file, both-forms-present error - New Design/CloudTeamCollections/notes/write-book-status-audit.md: 7-caller audit of WriteBookStatus for task 05 diff-dispatch design - Design/CloudTeamCollections/tasks/00-enablers.md: tick all 6 step checkboxes All 208 existing TeamCollection tests pass unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Creates the complete Supabase server-side implementation for Cloud Team Collections (Wave 1, task 01-server-schema): supabase/config.toml Standard Supabase CLI local-dev layout (as supabase init would produce). auth.email.enable_confirmations = false for local GoTrue auto-confirm (task 11 dev stack). Schema 'tc' exposed via PostgREST extra_search_path. supabase/migrations/20260706000001_tc_schema.sql tc schema + all tables: collections, members (with last-admin guard trigger and NULLS NOT DISTINCT unique constraint for claimed users), books (with partial unique index on live lower(NFC(name)), NFC-normalize trigger, lock columns, deleted_at tombstone), versions, version_files, collection_file_groups, collection_group_files, color_palette_entries, events (with realtime pg_notify broadcast trigger; type values mirroring C# BookHistoryEventType 0-9, plus incident type 100=WorkPreservedLocally), checkin_transactions. Helper functions: tc.jwt_email_verified() — the ONE place that reads email verification from the JWT (Firebase email_verified claim OR local GoTrue role=authenticated); tc.current_user_id(); tc.current_user_email(). User ids are TEXT throughout (Firebase UIDs ~28 chars; local-GoTrue UUIDs). All timestamps via now(). NFC-normalize applied to names and paths via triggers. supabase/migrations/20260706000002_tc_rls.sql RLS enabled on all tables. Policies: members SELECT/INSERT/UPDATE/DELETE gated on is_member/is_admin helpers; books/versions SELECT only (no direct writes); events SELECT + INSERT (own events); color_palette_entries SELECT + INSERT. is_member() and is_admin() SECURITY DEFINER helpers. GRANTs for authenticated role; anon role gets nothing. supabase/migrations/20260706000003_tc_rpcs.sql All CONTRACTS.md RPCs as SECURITY DEFINER Postgres functions: create_collection, my_collections, claim_memberships (guarded by jwt_email_verified()), get_collection_state (full/delta), get_changes (cursor-based), checkout_book (race-free conditional UPDATE), unlock_book, force_unlock (audited, emits type=5), delete_book (lock required, emits type=8), undelete_book (name-uniqueness enforced), rename_check (advisory), members_list/add/remove/set_role (remove force-unlocks + emits events; last-admin guard via trigger), add_palette_colors (insert-on-conflict-do- nothing), log_event. EXECUTE grants to authenticated role. supabase/tests/01_tc_schema_test.sql pgTAP suite (authored, unrun — Docker not available on this machine). Covers all acceptance criteria: schema/table existence, jwt_email_verified (Firebase + GoTrue paths), RLS matrix (non-member blocked), claim_memberships requires verified email, checkout concurrency (Alice wins / Bob loses / one event emitted), last-admin guard (delete + demote), get_changes cursor, tombstone/undelete, live-name uniqueness (tombstoned names reusable). Design/CloudTeamCollections/tasks/01-server-schema.md Tick all Step checkboxes; note Acceptance tests authored but unrun. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Delivers all authored artifacts for Cloud TC task 11 (local dev stack). Nothing in this commit touches Bloom application code. Files added under server/dev/: - docker-compose.yml: MinIO SNSD (object versioning ON) + minio-init job that creates bloom-teams-local bucket with versioning + 7-day lifecycle. Console at :9001, S3 API at :9000; fixed dev root credentials. - .gitignore: excludes minio-data/ from version control. - seed.sql: three dev users (admin/alice/bob @dev.local, password BloomDev123!) inserted directly into auth.users + auth.identities with email_verified=true so tc.jwt_email_verified() (task 01) is satisfied immediately. - config.auth.toml.snippet: auth settings (enable_signup=true, enable_confirmations=false) for orchestrator to fold into supabase/config.toml at merge (task 01 owns that file per coordination rule). - README.md: full bring-up/teardown/reset instructions, all BLOOM_CLOUDTC_* env var definitions, two-instances-on-one-machine recipe, prerequisite install commands for Docker Desktop / Supabase CLI / Deno on Windows. - DEV-CREDENTIALS.md: edge-function dev credential mode spec - when in dev mode functions return static MinIO root credentials in the IDENTICAL STS response shape (accessKeyId/secretAccessKey/sessionToken/expiration), so the C# client cannot tell the difference and CONTRACTS.md stays frozen. Documents all known MinIO/AWS parity gaps. - parity-check/ParityCheck.csproj + Program.cs: standalone .NET 10 console project (NOT in Bloom.sln) that verifies MinIO supports (a) PUT with x-amz-checksum-sha256, (b) server-side checksum readback, (c) version-id capture on PUT, (d) GET by (key, versionId). BUILD VERIFIED (dotnet build clean). UNRUN - requires Docker Desktop. - smoke.ps1: stack smoke test (GoTrue signup, MinIO versioned PUT via parity-check, download-start edge function call). AUTHORED-UNRUN. Also adds the Design/CloudTeamCollections/ files (from cloudTC branch) that provide context for this and all Wave 1 tasks. Task 11 checklist: all authored items ticked; smoke.ps1 and parity-check runtime marked AUTHORED-BUT-UNRUN pending Docker Desktop installation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reviewed: lock seams preserve folder behavior verbatim; 208/208 TeamCollection tests pass against fresh binaries (verified Bloom.dll in test output contains the new code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Authored-but-unrun (Docker not yet installed); review fixes follow in the next commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- checkout_book: GET DIAGNOSTICS ROW_COUNT must land in an integer, not a boolean (boolean > integer has no operator; every checkout would have failed at runtime). - pgTAP: claim_memberships unverified-email test now expects ERRCODE 28000, matching the function. - Documented that pg_notify does not reach Supabase Realtime; the realtime.send swap is deferred to the wave-4 realtime work per the polling-first plan. - config.toml: wire server/dev/seed.sql (task 11 dev users) via [db.seed]. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ity tools Authored; runtime verification pending Docker Desktop install. Seed-hash fix follows in the next commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hand-computed hash in task 11 did not match the documented password (verified with bcryptjs: compareSync returned false). Replaced all three copies with a generated, self-verified bcrypt(cost=10) hash for BloomDev123!. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contract clarifications (wire format only): RPC JSON keys use the p_ parameter prefix; tc-schema RPC calls need the Content-Profile: tc header. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First live run of everything tasks 01 + 11 authored, on Podman 5.8.3
(rootful WSL machine, Docker-compat pipe) with Supabase CLI 2.109.0:
- pgTAP 42/42 GREEN. Fixes: plan count (60 -> 42 actual assertions);
RLS-matrix assertions now run under SET LOCAL ROLE authenticated,
because the suite's postgres superuser bypasses row security and made
those tests pass vacuously.
- S3 parity spike 4/4 PASS (sha256 checksum PUT + server-side readback,
version-id capture, GET by version). Fix: use BasicAWSCredentials --
MinIO validates session tokens, so the fabricated devmode token was
rejected. DEV-CREDENTIALS.md spec corrected: dev-mode edge functions
must mint real temporary creds via MinIO AssumeRole.
- smoke.ps1 GREEN (signup + MinIO + functions-endpoint reachability).
Fixes: PS 5.1 parse errors (invalid ${()} interpolation; em-dashes in
a BOM-less UTF-8 file decode as CP1252 smart quotes), native-stderr
handling, and JSON output from newer supabase status.
- MinIO lifecycle now set via mc ilm rule add (JSON import was mangled
by compose quoting); idempotent via shell case (mc image has no grep).
- Committed .gitkeep in every bind-mounted dir (minio-data, supabase
snippets/functions): Podman does not auto-create missing bind-mount
directories. Added supabase/.gitignore for CLI state dirs.
- README prerequisites rewritten around the verified Podman recipe
(rootful requirement, compat pipe, npm-based CLI installs).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, CloudCollectionClient)
Implements the dev-provider-first design from Design/CloudTeamCollections/tasks/03-auth.md:
- CloudEnvironment resolves Supabase/S3/auth config from BLOOM_CLOUDTC_* env vars over
compiled defaults that point at the local dev stack.
- CloudAuth is a provider-agnostic session core (token store, proactive refresh at ~80% TTL,
refresh-on-401, sign-out, account-switch detection, env-override-wins-over-stored-session
startup path) plus CloudLoginState groundwork for the future sharing/loginState endpoint.
- DevCloudAuthProvider signs in against local GoTrue (sign-up-then-sign-in for unknown
emails); RealCloudAuthProvider is an inert "not yet available" stub pending the Option
A/B/C decision.
- CloudCollectionClient is a RestSharp client (modeled on BloomLibraryBookApiClient) for
tc-schema RPCs and edge functions: bearer injection, Content-Profile: tc header, one
refresh-and-retry on 401 then abort with NotSignedIn, and typed CloudErrorCode mapping
for the CONTRACTS.md error shapes (LockHeldByOther, ClientOutOfDate, etc.).
Live-verified against the local Supabase stack: dev sign-in/sign-up, RPC calls with the
Content-Profile: tc header, and the {code,message} error shape PostgREST returns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Durable-state protocol: each task lives on its own branch with per-step commits, checkbox ticks, and a progress log in the task file, so work survives AI-session interruption and any future session can resume from Design/CloudTeamCollections/orchestration/RESUME.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds CloudAuthTests, CloudCollectionClientTests, CloudEnvironmentTests under
src/BloomTests/TeamCollection/Cloud/, covering every item in 03-auth.md's Acceptance
section against fakes (no network): proactive refresh timer, refresh-on-401 success and
clean-abort-on-failure ("please sign in"), account-switch detection, env-override winning
over a stored session, bearer/apikey/Content-Profile header injection, and CloudErrorCode
mapping for every CONTRACTS.md error shape (LockHeldByOther, BaseVersionSuperseded,
NameConflict, MissingOrBadUploads, VersionConflict, ClientOutOfDate, plus the generic
Postgres {code,message} shape live-verified earlier).
Also adds one [Explicit] test, LiveDevProvider_TwoUsersSignInConcurrently_HoldDistinctSessions,
that exercises the real DevCloudAuthProvider against the running local Supabase stack (no
mocks) and confirms two dev users get independent, independently-refreshable sessions -
verified passing against the live stack in this session.
CloudCollectionClient gained a small test seam (IRestExecutor/RestSharpExecutor,
SetRestClientForTests) so the header-injection and error-mapping logic can be exercised
without a live server; production behavior (a real RestSharp RestClient) is unchanged.
FullyQualifiedName~Cloud: 46/46 green. FullyQualifiedName~TeamCollection regression
filter: 244/244 green (no folder-TC behavior change).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…view) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…all 6 edge functions New migration 20260706000004_tc_checkin_txn_functions.sql adds the SECURITY DEFINER transaction functions (checkin_start_tx/finish_tx/abort_tx, collection_files_start_tx/ finish_tx, download_start_check) that back the edge functions, plus a checkin_transactions table extension (proposed_files/checksum/result_version_id) and a new collection_file_transactions table. Uses PostgREST's PT### errcode convention so SQL exceptions map straight to HTTP status with a JSON body the edge functions unwrap. Authors all 6 edge functions under supabase/functions/ (checkin-start, checkin-finish, checkin-abort, download-start, collection-files-start, collection-files-finish) plus _shared/ helpers (env config incl. dev/prod S3 credential seam, PostgREST RPC client forwarding the caller's own JWT, error/response helpers). Applied cleanly via `supabase db reset --local`; `deno check` passes on all functions. Not yet exercised against the live stack — see the task file's Progress log for next steps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s found Ran a full live-integration check against the real local stack (Supabase + MinIO via `supabase functions serve --env-file server/dev/functions.env`) covering every item in the task's acceptance checklist except a real 48h expiry wait: happy-path checkin-start -> MinIO AssumeRole PUT -> checkin-finish -> download-start round-trip, idempotent finish retry, resume (new-book and existing-book), lock-held, base-version-superseded, checksum failure (MissingOrBadUploads), new-book invisibility until commit, and the collection-files two-phase commit + VersionConflict path. 26/26 checks passed after fixing: 1. `host.containers.internal`/`host.docker.internal` DNS-resolves fine but the traffic HANGS INDEFINITELY for any Deno/edge-runtime HTTP call through Podman's gvproxy host-gateway hop on this machine (verified with raw Deno.connect, native fetch, and the AWS SDK; plain curl over the identical URL succeeds instantly) -- likely what stalled the previous interrupted attempt at this task. Fixed by joining MinIO to the Supabase CLI's own project Docker network (server/dev/docker-compose.yml) and addressing it by container name (http://bloom-minio:9000) instead of the host gateway. 2. supabase/config.toml's generated `[edge_runtime].policy = "oneshot"` re-transpiles the whole module graph -- including the heavy npm:@aws-sdk/client-s3+client-sts imports -- on every request, reliably exceeding the ~10s worker-boot timeout. Switched to `per_worker`. 3. New-book checkin-start resume was unreachable: the SQL always took the insert-a-new-row path when bookId was null, so a client resuming an interrupted new-book Send (which has no bookId to send back, by design) always tripped its own prior row's instance-id-conflict check. Fixed to recognize "already exists, not yet committed, still locked to me" as a resume. 4. get_collection_state's full-snapshot branch (task 01's file) leaked uncommitted new books to every collection member; the delta branch was already safe. Fixed by excluding current_version_id IS NULL rows unless locked to the caller. Both networking/config fixes and both SQL fixes are documented in server/dev/README.md's new "Known gotchas" section and inline SQL comments. Remaining for this task: Deno unit tests per function, the transaction- lifetime-vs-noncurrent-expiry invariant test, and server/provision-aws. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each index.ts previously passed its request handler straight into
serveJsonPost(...), which calls Deno.serve at module scope -- meaning simply
importing the module (e.g. from a Deno test) would start a real server as a
side effect, with no way to invoke the handler logic directly or mock its
Request. Refactored all 6 functions to `export const handler = ...` plus
`if (import.meta.main) { serveJsonPost(handler); }`, so a test can import just
the handler and call it with a mocked Request/mocked fetch, while
`supabase functions serve` (which runs index.ts as the entry point) is
unaffected. Verified empirically against the running local stack that the
real supabase-edge-runtime does set import.meta.main = true for the served
module -- re-ran the full 26-check live-integration suite post-refactor,
still 26/26. `deno check` clean on all 6.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds hermetic, mocked-dependency Deno tests (no live stack required) to complement the live-integration spike already recorded in the Progress log: - _shared/test_support.ts: setTestEnv/mockRequest/withMockFetch/ routedFetchStub/callHandler test helpers. callHandler mirrors serveJsonPost's HttpError-to-Response translation so tests can call each handler directly. - _shared/s3.test.ts (8 tests): hexToBase64, getScopedCredentials (dev-mode MinIO AssumeRole shape, no session Policy in dev, missing-credentials failure), verifyUploadedObject (checksum match/mismatch/missing-object/ missing-VersionId), writeManifestBackup (never throws). S3/STS mocked via aws-sdk-client-mock. - One index.test.ts per function (22 tests total): happy path, required-field validation, and RPC error passthrough (409/404/426), asserting S3 credentials are never issued on an error path. PostgREST calls mocked via a fetch stub. - _shared/invariants.test.ts (2 tests): re-parses the actual migration/ compose source text (not hardcoded twice) to assert the 48h transaction- expiry interval is consistent everywhere and strictly less than the dev MinIO noncurrent-version-expiry floor (7d). 32/32 pass: `deno test --allow-net --allow-env --allow-sys --allow-read supabase/functions/`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the last remaining step of task 02. PowerShell, matching the only existing server/ script convention (server/dev/smoke.ps1). Idempotent (checks-then-creates everywhere): S3 buckets bloom-teams-production/sandbox with versioning ON, all public access blocked, and lifecycle rules (noncurrent-version-expiry 7d + abort-incomplete-multipart-upload 7d under tc/, mirroring server/dev/docker-compose.yml's MinIO init job); an IAM role bloom-teams-broker whose permission policy is the ceiling the edge functions' per-request AssumeRole session Policy narrows from; an assume-only IAM user bloom-teams-broker-caller (sole permission: sts:AssumeRole on that role) that becomes AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY; an admin IAM user bloom-teams-admin with direct S3 permissions backing adminS3Client(). Ends by printing the exact `supabase secrets set` command block for every env var _shared/env.ts's prodBrokerConfig/adminS3Credentials read. Per the task file, this is authored and reviewed only -- no AWS account was available to run it against (verified only via PowerShell's AST parser for syntax correctness). This completes all 5 steps + both acceptance items in Design/CloudTeamCollections/tasks/02-edge-functions.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Committed by orchestrator on resume to preserve in-flight work; the resumed agent continues from the task-file progress log. Pre-commit hook unavailable in worktree (husky-run); prettier pass owed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add useIsCloudTeamCollectionsExperimentalFeatureEnabled() to sharingApi.ts, reading app/enabledExperimentalFeatures for the cloud-team-collections token (matches ExperimentalFeatures.kCloudTeamCollections in C#). Wire a new "Share this collection on the Bloom sharing server (experimental)" button into TeamCollectionSettingsPanel.tsx's not-yet-a-TC branch; disabled with an explanatory tooltip until the flag is on, otherwise posts teamCollection/showCreateCloudTeamCollectionDialog (Wave-3 endpoint, mocked for now). Also add createCloudTeamCollection() for the next step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add CreateCloudTeamCollectionBody (presentational; sign-in -> immutable-name-acknowledgement -> initial Send progress -> done/error, all gated on props) and CreateCloudTeamCollectionDialog (container) to CreateTeamCollection.tsx. Dev-auth mode shows a plain email/password form; cloud mode shows a placeholder "Sign in with your Bloom account" button (real BloomLibrary browser flow lands in a later task). No folder chooser, no Dropbox checkboxes, no restart. Fix createCloudTeamCollection() to use postJson instead of post, since post() is fire-and-forget and does not return a promise. Add CreateCloudTeamCollection.test.tsx (10 tests) covering the step gating. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add MyCloudCollectionsSection.tsx: a presentational sidebar for the collection chooser listing the signed-in user's cloud Team Collections (claimed or not), with a signed-out sign-in prompt, loading, and empty states, plus a per-row pull-down button. Wire it into CollectionChooser.tsx next to the existing recent-collections grid, backed by useSharingLoginState/useMyCloudCollections/pullDownCollection. Add MyCloudCollectionsSection.test.tsx (4 tests). The pull-down-join dialog (JoinCloudCollectionDialog, six folder-TC scenarios + NotSignedIn + ApprovalRemoved) is the remaining half of this step and follows in the next commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add JoinCloudCollectionDialog.tsx: eight-state pull-down-join dialog, structurally mirroring the folder-TC JoinTeamCollectionDialog's six scenarios (create/merge/open/switch/conflict/incomplete-local-copy) plus two states unique to cloud collections: NotSignedIn (action button becomes "Sign In") and ApprovalRemoved (action disabled, explains the user isn't on the approved list). The join action calls pullDownCollection from sharingApi.ts. Add JoinCloudCollectionDialog.test.tsx (8 tests) covering all eight states. Not yet wired into CollectionChooser.tsx's onPullDown: picking the right state requires backend six-scenario matching logic that doesn't exist until a later task, so the chooser keeps calling pullDownCollection directly for now (Wave-1 shell, mocked endpoints). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add cloudAccountEmail to IRegistrationContentsProps/IRegistrationDialogProps, threaded through RegistrationDialog and both launchers. When set, the registration email field is force-synced to it and always locked (identity = the signed-in Bloom account, not free text), with a distinct label/note explaining why. Wire this into CreateCloudTeamCollectionDialog: startSend now checks registration/userInfo first (mirroring the folder dialog's tryToCreate) and shows the registration dialog with cloudAccountEmail set if not yet registered, before proceeding to the actual send. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Orchestrator-preserved in-flight work; 29 component tests were passing at interruption. Remaining per the agent: lint pass, progress log, final commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ress log - Ran yarn prettier --write on all 16 files this branch touched; already clean, so the two WIP commits that skipped the pre-commit hook needed no reformatting. - Re-ran the component test suite in single-run mode (yarn vitest run --pool=threads) covering SharingPanel, CreateCloudTeamCollection, MyCloudCollectionsSection, and JoinCloudCollectionDialog: 4 files, 29 tests, all passing. - Ran yarn lint project-wide: 0 errors; confirmed the handful of warnings that land in files this branch touched (CreateTeamCollection.tsx, vitest.setup.ts) sit on pre-existing lines outside this branch's diff. - Verified all new user-facing strings from this task go through XLF (DistFiles/localization/en only) per .github/skills/xlf-strings/SKILL.md: correct priority file, translate="no", ID notes, and translator-context notes where needed; no existing entries changed. - Ticked the remaining checkbox and closed out Design/CloudTeamCollections/ tasks/07-ui-setup.md's progress log; task 07 (Wave-1 shells) is complete.
…hestrator review) MyCloudCollectionsSection rendered (and queried) for every user; the plan keeps all cloud UI behind the cloud-team-collections experimental flag until GA. With the flag off, the chooser renders exactly its pre-cloud content. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…point The endpoint was registered per-project (TeamCollectionApi), but callers legitimately probe it when no project is open: the E2E harness polls it as a readiness signal (one such probe landed before project registration in Bob's e2e-3 relaunch log, 10 Jul 10:11:41 - BEFORE any WebView2 existed), and a closing collection tab can get a late request in while the chooser is on screen (John's on-screen sighting). Every such call raised the 'Cannot Find API Endpoint' NonFatalProblem toast. Moved the registration to SharingApi (application level, same file that already reaches the current project via TeamCollectionApi.TheOneInstance for members/history/etc.), answering all-false whenever no project/TC is current - which is the truthful answer at chooser time. TeamCollectionApiCloudTests' two capabilities tests now register SharingApi on the test server as well, keeping end-to-end coverage of the endpoint; fixture green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…claimed membership + modal report) Root cause, proven by a live dotnet-stack dump of the hung instance plus a rerun with better reporting: opening a collection under an APPROVED account that never ran the join flow (batch item 9's shared-computer scenario) passed CheckConnection's member check - my_collections matches by EMAIL, approved-or-claimed - and then threw not_a_member from get_collection_state during the constructor's first sync, because every data RPC's RLS gate matches by user_id and only the join flow (CloudJoinFlow) ever called claim_memberships. The generic catch in TeamCollectionManager's constructor then showed a MODAL MessageBox, which in --automation mode has no human to dismiss it: the instance sat blocked before any window or server existed, with completely empty stdout (e2e-10's bob-takeover 90s-timeout signature). Two fixes: - CloudTeamCollection.CheckConnection now calls ClaimMemberships (idempotent, once per session) as soon as membership is confirmed, so user_id-gated RPCs work no matter which account originally joined the local folder. - NonFatalProblem.Report in --automation mode writes the problem + stack to stdout (BLOOM_AUTOMATION_NONFATAL_PROBLEM, the harness's per-instance log) and returns instead of blocking on a MessageBox, mirroring the existing RunningInConsoleMode guard. Any future startup-path report is now a readable log line instead of a silent hang. New test: CheckConnection_SignedInAndAMember_ClaimsMembershipsOncePerSession. C# cloud filter 418/418 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Defect 3's fix made teamCollection/capabilities an application-level endpoint, so it now answers (truthfully all-false) while a project is still opening instead of erroring - and BLOOM_AUTOMATION_READY fires when the server starts listening, which can be before the Team Collection connects. The two post-relaunch checks that single-shot-asserted supportsSharingUi === true (twoInstanceSetup's bob-joined and e2e-2's equivalent) now use the same 20s expect.poll every other true-asserting site already used. The false-asserting pre-create checks (e2e-1, e2e-7, join-auto-open) are unchanged: all-false is exactly what they expect. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r-broad lock skip), outstanding-bugs list Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d for Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…wn-locked books Two bugs diagnosed from e2e-4's fully-logged failure (see the batch doc's OUTSTANDING BUGS #1): - CloudBookTransfer staged every download into the FIXED temp path %TEMP%\BloomCloudTCDownload, shared by every download in every Bloom process; TemporaryFolder.Dispose deletes the whole folder, so two concurrent instances (shared-machine TC use, and every two-instance E2E scenario) clobbered each other mid-copy ('Could not find file ... A5 Portrait.htm'). The staging folder name is now unique per call. - QueueMissingRepoBooksForBackgroundDownload skipped books locked by ANYONE, so a teammate checking a book out seconds after one transient download failure left it missing for the whole lock duration. The skip now only covers books locked by the CURRENT user (the genuine rename-mid-checkin edge); a teammate's lock does not block downloading committed content (same semantics as Receive). Tests: locked-by-me still skips; locked-by-teammate now downloads. Cloud filter 420/420 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… bug documented for John's decision Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ock skip
Two adjacent holes found by the preflight light-review pass over today's
defect fixes:
- The claim_memberships guard was a once-per-instance bool; an in-session
sign-out + sign-in as a different approved member (nothing disposes the
CloudTeamCollection on CloudAuth.AccountSwitched) would skip claiming for
the new account and resurrect the not_a_member startup failure. Now keyed
by email: each distinct account claims once.
- QueueMissingRepoBooksForBackgroundDownload skipped books locked by the
current user on ANY machine; the rename-mid-checkin edge it protects is
machine-local ('checked out here' everywhere else in the file means
lockedBy AND lockedWhere match), and SyncAtStartup fetches such a book on
restart anyway. The skip now also requires lockedWhere == CurrentMachine,
so a book you checked out on another computer still self-heals here.
Also: batch doc updated with e2e-5/e2e-8 PASS results (transient-infra
theory confirmed) and single-spec scoreboard.
New tests: CheckConnection_AccountSwitchMidSession_ClaimsAgainForTheNewAccount,
QueueMissingRepoBooks_BookLockedByCurrentUserOnAnotherMachine_StillDownloadsIt.
Cloud filter 422/422 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…b half needs gh auth) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (SQUASH-PLAN.md) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…meouts, resume runbook); bug-0 option-a sketch preserved Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@greptile-apps review (Claude Fable 5 agent: bypassing the 100-file limit per the bot's own instructions — this draft PR is the full Cloud Team Collections feature, 237 files.) |
…losed (Devin diff-too-large, Greptile bypass triggered, CodeRabbit not installed) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
| Filename | Overview |
|---|---|
| supabase/functions/checkin-start/index.ts | S3 prefix now scoped to DB-canonical instance_id read back from books table; fixes the prior finding where caller-supplied bookInstanceId could steer write credentials to an arbitrary prefix. |
| supabase/functions/checkin-start/index.test.ts | New test pins the mismatch case (caller sends one instance id, DB has another); all error passthrough paths also covered. |
| supabase/migrations/20260711000001_tc_reap_counter_fix.sql | Fixes reap_expired_checkin_transactions counter bug — introduces v_updated so the collection-file sweep ROW_COUNT no longer clobbers the book-sweep accumulation. |
| supabase/migrations/20260711000002_tc_takeover_error_codes.sql | Re-creates checkout_book_takeover with PT404/PT403 passthrough codes and JSON error bodies, matching the schema-wide convention; takeover logic is untouched. |
| supabase/tests/02_tc_checkout_takeover_test.sql | Test 4a updated to assert PT403 (was 42501), matching the fixed error code; full takeover scenario coverage included. |
| src/BloomExe/TeamCollection/Cloud/CloudTeamCollection.cs | New cloud-backed TeamCollection implementation; account-switch takeover wired correctly before check-in when the repo shows a same-machine/different-account lock. |
| supabase/functions/_shared/rpc.ts | callTcRpc and selectTcRow both forward the caller's own JWT; parsePostgrestErrorBody unwraps the PT### JSON message convention before surfacing errors. |
Reviews (2): Last reviewed commit: "Batch log: full-matrix attempt invalid (..." | Re-trigger Greptile
E2E: since progressive join (batch item 7), books new to an instance arrive via the background download queue AFTER a sync poll returns. e2e-6 read Bob's v1 baseline immediately after the poll (failed standalone; the file appeared on disk moments later) - now an expect.poll with the harness's 90s convention. The two 20s ceilings on queue-driven arrivals (e2e-6 v2, e2e-9 first test) also raised to 90s. e2e-3/6/9 all green standalone. Greptile (via the 100-file-limit bypass on PR 8048) found: - P1 security: checkin-start scoped S3 write credentials to the caller-supplied bookInstanceId, which checkin_start_tx never validates for existing books - any member could obtain write creds for any book prefix in their collection. Now reads the DB-canonical instance_id back (same pattern as checkin-finish); new deno test pins that a mismatched client value cannot steer the prefix. - P2: reap_expired_checkin_transactions returned only the collection-file sweep count (GET DIAGNOSTICS clobbered the loop total) - fixed in new migration 20260711000001. - P2: checkout_book_takeover raised P0002/42501 bare strings instead of the schema-wide PT404/PT403 JSON convention (C# client would map both to CloudErrorCode.Unknown) - fixed in new migration 20260711000002, logic untouched; pgTAP 4a expectation updated to PT403. - Style: JoinCloudCollectionDialog.tsx nested ternaries -> if/else chains. Suites: pgTAP 55/55, deno 33/33, dialog vitest 12/12, lint+prettier clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… CDP failures); rerun queued on unlock Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@greptile-apps review (Claude Fable 5 agent: re-triggering after b93d0c9, which fixes all three of your findings — please verify.) |
…ONTRACTS v1.5) Item 9's same-machine takeover gated only on machine name, so any same-machine account could silently steal any same-machine lock - even across two separate local copies of the collection (e2e-4's scenario, and any shared lab machine). John's ruling: editing/takeover of a checkout is only legitimate in the local copy where the book is checked out. Server (migration 20260711000003, additive): - tc.books.locked_seat: client-computed stable hash of the local collection folder path (never the raw path), recorded by checkout_book/checkout_book_takeover (new optional p_seat). - Takeover now requires machine AND seat match; a NULL stored seat never matches (fail-safe for pre-seat locks and checkin_start_tx's take-if-free path, which records no seat). - A BEFORE UPDATE trigger clears locked_seat whenever locked_by clears, covering every unlock path without recreating those functions. - get_collection_state/get_changes book rows carry locked_seat. Client: - CloudTeamCollection.SeatId (first 16 hex of SHA256 of the normalized folder path); passed on checkout and takeover. - IsEditableHere/CanTakeOverLockOnThisMachine seams now take bookName so the cloud override can consult the cache's LockedSeat: another account's lock is editable/takeoverable only in the same seat; the current user's own pre-seat (null-seat) locks are grandfathered. - CloudRepoCache rows/snapshots carry LockedSeat. Also: e2e-7's 20s first-commit poll raised to the 90s harness convention (its matrix failure was a load flake; standalone 2/2). Verified: pgTAP 65/65 (10 new seat cases), C# cloud filter 428/428 (6 new), e2e-4 PASS (cross-seat takeover refused), e2e-10 PASS (same-seat takeover intact). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Too many files changed for review. ( Bypass the limit by tagging |
The initial book commit runs asynchronously after createCloudTeamCollection; under matrix load it can still be in flight when the spec reaches section 4, and killing Alice mid-first-Send leaves no book row ever (11 Jul matrix, sole failure of 14; passed standalone). Also: batch log for the 13/14 post-seat-fix matrix (e2e-4 passed under full load). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…SS export fixes; no Cloud TC overlap) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Building the cloud-tc-for-review packaging branch re-ran the pre-commit formatter against origin/master as the base, exposing formatting drift in 14 edge-function files that had bypassed it (committed from subagent worktrees). This applies the same output here so the two branches are byte-identical. deno tests 33/33 after. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Superseded by #8052, which carries the identical tree (verified byte-identical against (Claude Fable 5 agent) |
…mits, byte-identical), #8048 closed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cloud-hosted Team Collections: a new TeamCollection backend storing book content in S3 and coordination state (locks, versions, membership) in Supabase (Postgres RLS + edge functions), with sign-in, sharing/membership UI, progressive join, automatic remote-update application, account-switch handling, and a Playwright-over-CDP E2E harness driving real two-instance Bloom sessions against a local dev stack.
Design docs: Design/CloudTeamCollections/ (architecture, CONTRACTS.md, GOING-LIVE.md). Durable work-state and outstanding items: Design/CloudTeamCollections/orchestration/DOGFOOD-BATCH-1.md.
Known open item at draft time: item 9's same-machine lock takeover fires across separate collection folders (batch doc OUTSTANDING BUGS #0, decision pending) - e2e-4 fails its final assertion on this; all other E2E singles and the full C# suite are green.
🤖 Generated with Claude Code
Devin review
This change is