Store: declared transfers between records (#902) - #926
Merged
Merged
Conversation
- transfers.<name>: {amount, min?, members?} on a collection serves
POST <mount>/transfers/<name> with {from, to, amount}: one BEGIN IMMEDIATE
transaction subtracts amount from from's integer property and adds it to
to's, with both audit events (store.record.transferred, side/counterpart)
and the Idempotency-Key claim, so the sum never changes.
- Body validated by a generated schema (two distinct record ids, a positive
whole amount <= 2^53-1); anything else, a fraction included, is
422 invalid_transfer. Below min (default 0) is 409 insufficient_balance;
outside the property schema or safe integers 409 transfer_limit (no issue
list); every refusal writes nothing. If-Match on from; replay answers both
records as they are now.
- Authorization: owned collection debits only the caller's own record and
may credit any owned record (to shown only when the caller owns it); the
floor is checked before to is read, so ids cannot be probed for free.
Shared: anyone reaching the mount. members gates who may run it; a gated
transfer with a negative min is an issuer (double entry).
- Amount property: required integer with integer default, not an increment
or intervals property; a readOnly property a transfer moves is accepted.
- StoreRecords.transfer and StoreTransactionRecords.transfer; OpenAPI path
with Store<C>Transfer/Store<C>Transferred.
- Tests: 200 interleaved transfers conserve the total in one process and
across four connections, overdraft/412/422 write nothing, owned/shared/
members authorization, same-record, retry replay, trigger-injected audit
rollback, activation refusals, OpenAPI validity and served answer.
- STORE.md "Declared transfers" section and "What is not covered", README,
llms, CHANGELOG, package budgets re-measured.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Implements #902 item 2: a declarative transfer between records that preserves the collection's sum. Before this, it needed a trusted
StoreExports.transaction.Declaration
collections.<c>.transfers.<name>: {amount, min?, members?}, served atPOST <mount>/transfers/<name>with body{from, to, amount}.422 invalid_transfer.integer, so values are minor units and nothing is rounded.intervals, and transfers on a membership collection. At most 8 transfers per collection.Execution
Everything runs in one
BEGIN IMMEDIATEtransaction, and every refusal writes nothing. The checks run in this order:fromis in the caller's scope: 404from: 412min(default 0): 409insufficient_balancetoexists: 404transfer_limit, with no issue liststore.record.transferredaudit events (names and ids only, never amounts)StoreRecords.transferandStoreTransactionRecords.transferfollow the same rules.Authorization model
tois returned only when the caller owns it.tois looked up, so ids can't be probed without funds.members.members: narrows who may transfer; it never allows debiting someone else's record.minacts as an issuer, whose negative balance is the supply outstanding.Evidence (macOS, Node 26), on main after #925
transfers.test.ts(9 tests):openapi.test.ts: the document validates, and a live answer validates againstStore<C>Transferred.npm run verifyandnpm run test:packagepass. The store workspace has 172 tests.Limits
transfer_limitreveals that a recipient is near itsmaximum; this is documented.🤖 Generated with Claude Code