Repository navigation
fix(review): #1294 review findings, round 1 - #1296
Merged
SudoThijn merged 6 commits intoOct 20, 2026
Merged
Conversation
…sion to restore #1294 (comment) ShareSyncService::writeCopyBlobs() now keeps the source's state before a write by anyone but its owner as a version (SecretVersionService::snapshot, actor `user:<writer>`), under the same rule SecretService applies to the owner's own update: only when a value changes and the source held one. SecretVersionService is an appended, nullable, DI-resolved constructor parameter of ShareSyncService. Only the source is snapshotted: secret-version-history keeps the canonical history on the owner's copy and scopes a recipient's history to their own copy, and the owner's own update snapshots no recipient copy either. The caller's own copy is already snapshotted by SecretService::update(). Class sweep: the same snapshot covers every non-owner write of the source, on share-save and on /sync: a group member, a team-folder writer and a delegate. The owner's own /sync takes none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ave's guards #1294 (comment) A member of a group the secret is shared with (GROUP_WRITE) now gets share-save's three guards on PUT /secrets/{id}/sync: - expectedUpdatedAt '' is refused, so no blind overwrite; - the writer is checked again inside atomic(), after the source lock, so a leave or a group-share revoke that committed meanwhile refuses the sync; - the updates must name the source and every copy whose access has not ended (an ended or gone copy may be named and is not written), as ShareSaveScope::assertComplete() requires; ShareSaveScope depends on ShareSyncService, so the check is mirrored, not called. The owner's, a delegate's and a team-folder writer's /sync are unchanged. GroupMemberWritesForEveryoneTest's successful group-member syncs now pass a real base. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…update propagates #1294 (comment) user-sharing/spec.md: the Sync on Update requirement (L194) takes the review's suggestion and names delegates; the Purpose (L12) and the acceptance criterion (L440) say the same instead of "either party". Class sweep: no other live copy of the sentence in docs/, README or openspec/changes/ (only archived changes, which stay as recorded). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1294 (comment) - GroupMemberWritesForEveryoneTest: a member whose copy came through a team folder (with a `read` grade there) and who is in a group the secret is shared with gets `write` and saves for everyone. - ShareSaveServiceTest: a member who left the group, or whose group share was revoked, between resolve() and the lock is refused under the lock and the save rolls back. groupAuth() answers isInGroup / findBySecret differently once the save's transaction began, as goneUnderLock does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…se-only before the sync's lock #1294 (comment) assertGroupWriteGuards() checked the writer again on the freshly loaded source but not, as share-save's assertStillWriter() does through assertWritable(), whether that source became a read-only or use-only copy meanwhile: neither moves the version the lock checks. It now refuses both, with the same refusals syncUpdate() gives before the lock (ForbiddenException for read-only, 'A use-only copy cannot be changed'). assertWritable() is private to ShareSaveScope, which depends on ShareSyncService, so the two checks are mirrored, not called. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ly for a holder without a vault key #1294 (comment) assertGroupWriteGuards() required every live copy to be named but not to carry a value, so a group member's /sync could name a copy with no blob: it got only the new updatedAt and kept the old ciphertext, and its holder read a stale value under a fresh version. Share-save's rule is followed: a copy may be left without the new value only when its holder has no vault key (ShareSaveScope::refusedSkips()), and then its row is not written at all. A named row with no non-empty key, login or additionalFields is now refused when its holder (the owner for the source) has an active key pair, and otherwise dropped from the rows to write, so its version does not move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes for the round-1 review of #1294 (#1294 (review)). Merge into
fix/1285-group-member-share-save, then re-request the review; nothing here is merged by the reviewer.Findings → commits
b25708a2ShareSyncServicetakes an appended, nullableSecretVersionService;writeCopyBlobs()snapshots the source's pre-update state, inside the transaction, before anyone but its owner overwrites it (share-save and/sync, so team-folder writers and delegates too). Only when a value changes, asSecretService::shouldSnapshot(); the owner's own save takes no extra snapshot. Only the source gets a version:secret-version-historykeeps the canonical history on the owner's copy./syncopened to group members without share-save's guardsaceeb9d0,b6c14936,2d90c220expectedUpdatedAtis refused; under the lock the source is loaded again and the caller re-checked (read-only, use-only, still a writer); the source and every live copy must be named. A copy may go without the new value only when its holder has no vault key (share-save'srefusedSkipsrule), and then its row is not written. The two tests that synced with''now pass a base.d0ee2e51git grep "either party" -- openspec/specsis empty.47a16bedwrite; membership lost, or the group share revoked, betweenresolve()and the lock refuses the save.Left to the author (threads stay open)
/syncfor team-folder writers and delegates keeps the old behaviour (empty base, a subset of copies, no re-check under the lock), which existing tests rely on; a source withupdatedAtnull takes no lock;/syncaccepts an emptykeynext tologin/additionalFields, which share-save refuses (assertKeyNotEmpty).Checks run on this branch
phpunit -c phpunit-unit.xml) → 2365 tests, OK, 46 skipped (database-backed).GroupMemberWritesForEveryoneTest::testAGroupMembersSyncKeepsTheSourcesPreviousValueAsAVersion,ShareSaveServiceTest::testAWriteGradeMembersSaveKeepsTheSourcesPreviousValueAsAVersionand::testAGroupMemberWritesTheSourceAndTheOtherCopiesAndIsAuditedAsAGroupWritered; allowing the empty base turnstestAGroupMembersSyncWithoutABaseIsRefusedred; dropping the guards under the lock turns four tests red; dropping the read-only and the use-only re-check each turns its own test red; accepting a valueless copy of a holder with a vault key, or writing a keyless holder's copy, each turns its own test red.composer phpcs(lib/) → 0 errors.b6c14936and2d90c220, round 2 clean.ShareSaveVersionLockTest), phpstan, psalm and the e2e spec; CI runs them.🤖 Generated with Claude Code