mirror of
https://github.com/Twigpine/node.git
synced 2026-11-11 05:01:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
966b2b8e0a | ||
|
|
2f99ad8555 | ||
|
|
3840b7392a | ||
|
|
9f4f509619 |
No files matched your search
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.8.0"
|
||||
".": "0.7.1"
|
||||
}
|
||||
-183
@@ -1,188 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [0.8.0](https://github.com/Twigpine/node/compare/v0.7.1...v0.8.0) (2026-11-11)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **node:** Ucan::verify_chain returns Result<Did> instead of Result<()>.
|
||||
* **node:** pending_ref_transitions.first_ref_name is dropped. Callers that read or write that column must move to receive_pack_requests.accepted_ordinal.
|
||||
* **node:** gate agent-task reads behind visibility rules ([#553](https://github.com/Twigpine/node/issues/553))
|
||||
* **node:** the GraphQL `repos` field now returns an error instead of the full list when more than MAX_VISIBLE_REPO_PAGE_SIZE (200) repositories are visible; larger lists must use `reposPage` with `limit`/`after`. Repository names are additionally limited to 100 bytes at creation, fork, and mirror-registration time.
|
||||
* **node:** gl cert show <id> --verify now requires --expect-node <did>; the node's self-reported DID is no longer trusted as a cert anchor.
|
||||
* **node:** `git-receive-pack` now rejects a push whose authenticated DID is not the repo owner, returning 403 before any ref update is applied. Delegated and CI keys count as non-owners: a UCAN `git/push` capability is verified but not yet honored for authorization, so an agent pushing under its own DID cannot push while this is on. Set `GITLAWB_ENFORCE_OWNER_PUSH=false` during a rolling upgrade, or have automation push as the repo owner, until scoped collaborator / UCAN-delegated push rights land.
|
||||
|
||||
### Features
|
||||
|
||||
* **core:** carry the resumed candidate's identity in the scan token ([2656718](https://github.com/Twigpine/node/commit/2656718916ab9c68dd7fccb8f6fba8bba6b43e82))
|
||||
* **node,git:** cap concurrent served git ops with a 503 load-shed ([#62](https://github.com/Twigpine/node/issues/62)) ([5981257](https://github.com/Twigpine/node/commit/5981257be582fe1d39980bb2b1b3ff5c98448e84))
|
||||
* **node:** add scheduled inbound mirror worker ([#555](https://github.com/Twigpine/node/issues/555)) ([209618a](https://github.com/Twigpine/node/commit/209618a2feb1157a5bd685282de3b68a64e3882b))
|
||||
* **node:** anchor the UCAN proof chain and honour delegated git/push ([#567](https://github.com/Twigpine/node/issues/567)) ([3a0d91a](https://github.com/Twigpine/node/commit/3a0d91a80176945ab711d442ca2d8acc95062b80))
|
||||
* **node:** persist upstream mirror configuration and transition state ([#538](https://github.com/Twigpine/node/issues/538)) ([0e5a160](https://github.com/Twigpine/node/commit/0e5a1608e21f4859b2919307287747263f9fa8d0))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api,sync:** Delimit positional arguments in git clone mirror invocations ([#539](https://github.com/Twigpine/node/issues/539)) ([0cdb4bc](https://github.com/Twigpine/node/commit/0cdb4bcc7c4f7b0eb997fc5ea26981b1e0e26e3c))
|
||||
* **attest:** bound did:key method-id length before base58 decode ([#360](https://github.com/Twigpine/node/issues/360)) ([#535](https://github.com/Twigpine/node/issues/535)) ([9b915b0](https://github.com/Twigpine/node/commit/9b915b09a7c727af15c5c520107403b9dc93d35a))
|
||||
* **attest:** make verify_all lenient under RequireAll ([#359](https://github.com/Twigpine/node/issues/359)) ([#532](https://github.com/Twigpine/node/issues/532)) ([db98b39](https://github.com/Twigpine/node/commit/db98b3957a15eff9e9f286f8a2d084d8e731fcdb))
|
||||
* **attest:** use strict Ed25519 verification with regression test ([1fcbbbd](https://github.com/Twigpine/node/commit/1fcbbbd89c027ae7d1d903f705631d94f8dfbadc))
|
||||
* **ci:** add tested release tag resolver ([1b65f70](https://github.com/Twigpine/node/commit/1b65f70403ab47ced8c2828745c1c79c89a185e4))
|
||||
* **ci:** recognize namespaced test attributes in pr-triage's needs-tests check ([1989fcc](https://github.com/Twigpine/node/commit/1989fcca112d0efb56978efe09d9a836afa7cb59))
|
||||
* **ci:** reject unsafe release dispatch tags ([7b365b0](https://github.com/Twigpine/node/commit/7b365b0dc3ef3b5e3d14fd62bccc434e57200612))
|
||||
* **ci:** reject unsafe release dispatch tags ([0b990d6](https://github.com/Twigpine/node/commit/0b990d687f0eb379b62eb600ede2a6233017e419))
|
||||
* **core:** cap the blinded recipient scan in open_blob ([#419](https://github.com/Twigpine/node/issues/419)) ([#440](https://github.com/Twigpine/node/issues/440)) ([2450bbd](https://github.com/Twigpine/node/commit/2450bbd9e962f47b374b07932a1ff3fc969b9427))
|
||||
* **core:** compute created-skew in checked arithmetic ([#358](https://github.com/Twigpine/node/issues/358)) ([#545](https://github.com/Twigpine/node/issues/545)) ([61d8a4f](https://github.com/Twigpine/node/commit/61d8a4f8f1503ca0b088a039802a8999d305642d))
|
||||
* **core:** count distinct signer DIDs in certificate threshold check ([3993fd1](https://github.com/Twigpine/node/commit/3993fd1f5daacbebb8f8e2c62af8856c493ec87a))
|
||||
* **core:** Count distinct signer DIDs in certificate threshold check ([50d3cbb](https://github.com/Twigpine/node/commit/50d3cbbe97f77b3cab9fab220add9f3a0d0dbc2f))
|
||||
* **core:** enforce strict RFC 8032 Ed25519 signature verification ([425ebf4](https://github.com/Twigpine/node/commit/425ebf40e7737f44c5fe4e9dc8995c8aeb074ce1))
|
||||
* **core:** ignore invalid signatures in satisfies_threshold ([#349](https://github.com/Twigpine/node/issues/349)) ([#554](https://github.com/Twigpine/node/issues/554)) ([0919cf7](https://github.com/Twigpine/node/commit/0919cf7c621e360fbdacf6b9162d46620edc0f86))
|
||||
* **core:** put the redirect predicate behind a feature so core stays lean ([c82a70a](https://github.com/Twigpine/node/commit/c82a70ab21a21192b2579693b7d7c13aeb8a4d20))
|
||||
* **core:** refuse a redirect that rewrites the signed request-target ([5c0d6f3](https://github.com/Twigpine/node/commit/5c0d6f32522adea8f167f8e445a26377b6fe3963))
|
||||
* **core:** reject small-order Ed25519 keys at DID resolution and before X25519 ([#314](https://github.com/Twigpine/node/issues/314)) ([86c544a](https://github.com/Twigpine/node/commit/86c544a577f28efb2341a53d4faa6b54d1c8b919))
|
||||
* **core:** reject zero threshold, canonicalize did:key, document replay ([#509](https://github.com/Twigpine/node/issues/509)) ([#540](https://github.com/Twigpine/node/issues/540)) ([b4a3122](https://github.com/Twigpine/node/commit/b4a3122500e87d0b84aa3b116171de070188a6bd))
|
||||
* **core:** share the origin-scoped redirect rule with the git helper ([ba44a01](https://github.com/Twigpine/node/commit/ba44a018ec8945b447977d89fc6a10d98fc1bb7b))
|
||||
* **core:** use strict Ed25519 signature verification ([a6c13be](https://github.com/Twigpine/node/commit/a6c13be15b1421f3791e49a8219ea739f1e3ec6e))
|
||||
* **deps:** bump aws-smithy-json to 0.62.7 (GHSA-8ffr-xgwf-xj56) ([#516](https://github.com/Twigpine/node/issues/516)) ([151d688](https://github.com/Twigpine/node/commit/151d68838743a96198879b3154cfbaedca04c3cd))
|
||||
* **deps:** bump follow-redirects and override ws in scripts/ ([#525](https://github.com/Twigpine/node/issues/525)) ([a23c81d](https://github.com/Twigpine/node/commit/a23c81d75ae0a7567905ca9ad093bc9ad7c5adcf))
|
||||
* **deps:** bump libp2p to pick up hickory 0.26 and serde_with 3.22 ([#518](https://github.com/Twigpine/node/issues/518)) ([623b600](https://github.com/Twigpine/node/commit/623b600835d717fcfdab36af116dd4b9a92a92cd)), closes [#76](https://github.com/Twigpine/node/issues/76)
|
||||
* **deps:** resolve cargo audit failures on main ([#514](https://github.com/Twigpine/node/issues/514)) ([4d024e0](https://github.com/Twigpine/node/commit/4d024e01bcf25c93a3b878ac50e6435575dcc5f4))
|
||||
* **git-remote:** abort fetch on EOF without POSTing; harden the real-git test harness ([#192](https://github.com/Twigpine/node/issues/192)) ([1cb930c](https://github.com/Twigpine/node/commit/1cb930cbd57ea5d232edddc8e50dc94fab4362a5))
|
||||
* **git-remote:** drive multi-round fetch as a v0 stateless-RPC client loop ([#117](https://github.com/Twigpine/node/issues/117)) ([5fdf155](https://github.com/Twigpine/node/commit/5fdf155c7fd7398f61c200c256cb7327729f378a))
|
||||
* **git-remote:** drive multi-round fetch as a v0 stateless-RPC client loop ([#117](https://github.com/Twigpine/node/issues/117)) ([a64b141](https://github.com/Twigpine/node/commit/a64b14150dfb7e369c16e5fab98375ba0ce38404))
|
||||
* **git-remote:** normalize the shim's accepted socket to blocking ([10e0840](https://github.com/Twigpine/node/commit/10e0840dd95204d2b8faa1c594808e2ad57bc9c4))
|
||||
* **git-remote:** normalize the shim's accepted socket to blocking ([#192](https://github.com/Twigpine/node/issues/192)) ([8d7f95f](https://github.com/Twigpine/node/commit/8d7f95fd63769ef30540f216190e1bb52ccd4f69))
|
||||
* **git-remote:** reject malformed pkt-line length headers instead of flushing ([8c04043](https://github.com/Twigpine/node/commit/8c04043fb56b5030a0aa6b328c91f20173352aa6))
|
||||
* **git-remote:** skip content-free flush rounds in the fetch loop ([#117](https://github.com/Twigpine/node/issues/117)) ([68ba492](https://github.com/Twigpine/node/commit/68ba49267c8d65169ab9adf0552761f87baba4c1))
|
||||
* **git-remote:** stop waiting on EOF for a delete-only push ([#442](https://github.com/Twigpine/node/issues/442)) ([c2e175d](https://github.com/Twigpine/node/commit/c2e175d2222975eccb4776447268e8678fd48ba4))
|
||||
* **gl:** accept the resume token the incomplete-scan message tells you to pass ([181f617](https://github.com/Twigpine/node/commit/181f6178a0e0f5b9506093fa7579ad6ef8dd2ff5))
|
||||
* **gl:** assert 404 verdict and harden transport error handling in whoami ([#220](https://github.com/Twigpine/node/issues/220)) ([a329174](https://github.com/Twigpine/node/commit/a3291741757038307e61d3952e6c20aa4901c8a3))
|
||||
* **gl:** assert whoami 404 verdict and preserve transport error cause ([#220](https://github.com/Twigpine/node/issues/220)) ([b4007a5](https://github.com/Twigpine/node/commit/b4007a5e005c63aa0c083b969cd8b5f65b918adb))
|
||||
* **gl:** close two completeness-gate escapes; surface the node's error key ([6b2c53b](https://github.com/Twigpine/node/commit/6b2c53bbb86bcb35a54ef2edd2078e8f208263a6))
|
||||
* **gl:** derive View: URL from node instead of hardcoding gitlawb.com ([#370](https://github.com/Twigpine/node/issues/370)) ([#549](https://github.com/Twigpine/node/issues/549)) ([d84c445](https://github.com/Twigpine/node/commit/d84c445f94c05e43994c8199bb8e4cd432554fbc))
|
||||
* **gl:** follow scan continuations in gl ipfs get, bounded three ways ([9a8d190](https://github.com/Twigpine/node/commit/9a8d190492315cf767807dd1a98470debff4bd25))
|
||||
* **gl:** make doctor exit non-zero on Fail-class checks ([#357](https://github.com/Twigpine/node/issues/357)) ([#547](https://github.com/Twigpine/node/issues/547)) ([42cc893](https://github.com/Twigpine/node/commit/42cc893fb3af17468767cd8c6722894cd4ef7e44))
|
||||
* **gl:** only report registered: false on 404 in whoami ([#220](https://github.com/Twigpine/node/issues/220)) ([21c74c6](https://github.com/Twigpine/node/commit/21c74c65d985200221489fa342d7419e8626362f))
|
||||
* **gl:** only report registered: false on 404 in whoami ([#220](https://github.com/Twigpine/node/issues/220)) ([fd78cfa](https://github.com/Twigpine/node/commit/fd78cfa3ad0e7997b284371a55b7fb52753236d1))
|
||||
* **gl:** only report registered: false on 404 in whoami ([#220](https://github.com/Twigpine/node/issues/220)) ([3cc15cf](https://github.com/Twigpine/node/commit/3cc15cf7247396ab1efcd279d3dd51dd8deaf014))
|
||||
* **gl:** only report registered: false on 404 in whoami ([#220](https://github.com/Twigpine/node/issues/220)) ([16cc3b3](https://github.com/Twigpine/node/commit/16cc3b37e8268d048f3145ab16978dae297df5d4))
|
||||
* **gl:** only report registered: false on 404 in whoami ([#220](https://github.com/Twigpine/node/issues/220)) ([3f41df5](https://github.com/Twigpine/node/commit/3f41df5868838544876473c2560c3d6be20e679c))
|
||||
* **gl:** only report registered: false on 404 in whoami ([#220](https://github.com/Twigpine/node/issues/220)) ([b0e5cc0](https://github.com/Twigpine/node/commit/b0e5cc0ee0e91ebe35689bb1e073fa7e48ab37ee))
|
||||
* **gl:** only report registered: false on 404 in whoami ([#220](https://github.com/Twigpine/node/issues/220)) ([12bc759](https://github.com/Twigpine/node/commit/12bc759ea6b20ab97c9dfeb4e00d7c6a6ca4fcfe))
|
||||
* **gl:** only report registered: false on 404 in whoami ([#220](https://github.com/Twigpine/node/issues/220)) ([10e3492](https://github.com/Twigpine/node/commit/10e34928254205fafa8e96513a3df7899bfb10b1))
|
||||
* **gl:** only report registered: false on 404 in whoami ([#220](https://github.com/Twigpine/node/issues/220)) ([c10f2ca](https://github.com/Twigpine/node/commit/c10f2ca255d63331f6acf3a29629e2a1c11ff355))
|
||||
* **gl:** only report registered: false on 404 in whoami ([#220](https://github.com/Twigpine/node/issues/220)) ([3add535](https://github.com/Twigpine/node/commit/3add5356e36f0c2f89a0617fcb8018209bf722ff))
|
||||
* **gl:** only report registered: false on 404 in whoami ([#220](https://github.com/Twigpine/node/issues/220)) ([fdd1103](https://github.com/Twigpine/node/commit/fdd11030dc55a68142441ecf05f0bf33a8d38695))
|
||||
* **gl:** pass the mirror temp path to git as an OS string ([#434](https://github.com/Twigpine/node/issues/434)) ([df87b15](https://github.com/Twigpine/node/commit/df87b150fdb00e4b70e359996a41b2eebf7bb426))
|
||||
* **gl:** pin secret-file modes at creation instead of chmod after ([#354](https://github.com/Twigpine/node/issues/354)) ([#557](https://github.com/Twigpine/node/issues/557)) ([a00d7d5](https://github.com/Twigpine/node/commit/a00d7d5c482f013ff103487793b445f636d3a1da))
|
||||
* **gl:** read whoami error bodies through CappedBody.text ([bfc44f9](https://github.com/Twigpine/node/commit/bfc44f926d08c0bf774e2c05dd76b245871294f1))
|
||||
* **gl:** read whoami error bodies through CappedBody.text ([4725146](https://github.com/Twigpine/node/commit/4725146db2c13cd3b8f998260d9df3a4b5f70c9e))
|
||||
* **gl:** report quickstart registration failure when the node returns no UCAN ([#355](https://github.com/Twigpine/node/issues/355)) ([#556](https://github.com/Twigpine/node/issues/556)) ([10d5b98](https://github.com/Twigpine/node/commit/10d5b98b2bb2ac221b5407321290e4e681b206f1))
|
||||
* **gl:** report why the node-DID comparison was skipped in cert show ([6b2c53b](https://github.com/Twigpine/node/commit/6b2c53bbb86bcb35a54ef2edd2078e8f208263a6))
|
||||
* **gl:** require non-empty error message and use ephemeral port in whoami tests ([#220](https://github.com/Twigpine/node/issues/220)) ([a27a267](https://github.com/Twigpine/node/commit/a27a2678712cc512b316f143d8c053dbdf981a4b))
|
||||
* **gl:** resolve webhook owner from caller identity, not node DID ([#568](https://github.com/Twigpine/node/issues/568)) ([f67688e](https://github.com/Twigpine/node/commit/f67688eae298ef7b9d07cfa16bbca3117240eb6f))
|
||||
* **gl:** sanitize full transport error chain in whoami, remove raw error attachment ([#220](https://github.com/Twigpine/node/issues/220)) ([39ce655](https://github.com/Twigpine/node/commit/39ce655904f516094cbbef6f2ee585906ef77e05))
|
||||
* **gl:** status-check the client read/write surfaces so a node denial surfaces as an error, not a fake result ([#123](https://github.com/Twigpine/node/issues/123)) ([#186](https://github.com/Twigpine/node/issues/186)) ([6b2c53b](https://github.com/Twigpine/node/commit/6b2c53bbb86bcb35a54ef2edd2078e8f208263a6))
|
||||
* **gl:** stop carrying request signatures to a host the node redirected us to ([de3f230](https://github.com/Twigpine/node/commit/de3f230940f21d88da48a972bddeb877420d2165))
|
||||
* **icaptcha-client:** parse GITLAWB_ICAPTCHA_INSECURE as truthy only ([#227](https://github.com/Twigpine/node/issues/227)) ([#533](https://github.com/Twigpine/node/issues/533)) ([77ac103](https://github.com/Twigpine/node/commit/77ac1037cbf893bf0e2e861753cc4bc78cc9053a))
|
||||
* **icaptcha:** return unsolved instead of blocking or overflowing ([#444](https://github.com/Twigpine/node/issues/444)) ([5479fd5](https://github.com/Twigpine/node/commit/5479fd59b848f13e7c0f0f1054b0d41cf50cbb73))
|
||||
* **infra:** serialize the upgrade and write compose through a unique temp path ([888fab6](https://github.com/Twigpine/node/commit/888fab602828dc8b514cfa5fbb4d027a3a2db019))
|
||||
* **ipfs:** page the legacy CID scan instead of materializing every repo ([4985e38](https://github.com/Twigpine/node/commit/4985e38af2bd629fa81ec70aec63937ed9894b73))
|
||||
* **node,infra,macos:** make the owner-push opt-out reach every launch path ([1f0d9ff](https://github.com/Twigpine/node/commit/1f0d9ff368c0169048aeec219de4114c2630dff6))
|
||||
* **node:** ANS-104 codec and recovery policy types ([#26](https://github.com/Twigpine/node/issues/26) split 2/4) ([#524](https://github.com/Twigpine/node/issues/524)) ([2c341f7](https://github.com/Twigpine/node/commit/2c341f71ac66c0011a09ff5864e8734fd0348c09))
|
||||
* **node:** authenticate gossip ref-update events before writing them ([#325](https://github.com/Twigpine/node/issues/325)) ([9bda2e1](https://github.com/Twigpine/node/commit/9bda2e103c9ae25a5a798343164b4fa02219735b))
|
||||
* **node:** bind a peer row to its DID so only its keyholder can repoint it ([#273](https://github.com/Twigpine/node/issues/273)) ([8d38043](https://github.com/Twigpine/node/commit/8d38043a4ca7d0df3c9a4c4bdbb171d7adcdec4f))
|
||||
* **node:** bound federated repository aggregation ([#546](https://github.com/Twigpine/node/issues/546)) ([0189990](https://github.com/Twigpine/node/commit/0189990ac9e72bed3c42bc589347eacec04be740))
|
||||
* **node:** bound GraphQL query cost ([#552](https://github.com/Twigpine/node/issues/552)) ([c805e35](https://github.com/Twigpine/node/commit/c805e35f40873a120c81bf00d9ddb50da956fd97))
|
||||
* **node:** bound Identify-derived Kademlia addresses ([#544](https://github.com/Twigpine/node/issues/544)) ([b33562f](https://github.com/Twigpine/node/commit/b33562f1a913cbc1b294f00577f4ab1f7ad67984))
|
||||
* **node:** bound legacy readiness fallback ([5af14c2](https://github.com/Twigpine/node/commit/5af14c203d07fdd2d0b5689fcb12e189237dbbb2))
|
||||
* **node:** bound peer failure tracking ([74f7554](https://github.com/Twigpine/node/commit/74f75548941712573d344aa058d162f40189b4df))
|
||||
* **node:** bound replay of authenticated gossip ref-update events ([#334](https://github.com/Twigpine/node/issues/334)) ([206e082](https://github.com/Twigpine/node/commit/206e0827ad6beb051cbb0fb5ef459a63a102c454))
|
||||
* **node:** bound the fruitless reads one sweep run will spend ([e4aaae8](https://github.com/Twigpine/node/commit/e4aaae8c5229355010ca710abf22c181210201c3))
|
||||
* **node:** bound the legacy CID scan and let the caller carry its position ([22f4db9](https://github.com/Twigpine/node/commit/22f4db94b2fb3b3c7c43c5bd34608ea77ce5d197))
|
||||
* **node:** bound the legacy scan's query by the remaining row budget ([21e6873](https://github.com/Twigpine/node/commit/21e687368336fd219d82308ee20b3d6bb018a3dc))
|
||||
* **node:** bound the legacy-repair read by the pin batch deadline ([ea2219b](https://github.com/Twigpine/node/commit/ea2219b2cb635da61f120f65434db4c427494f6c))
|
||||
* **node:** bound the pin loops' DB work by the batch deadline, both lanes ([9984e08](https://github.com/Twigpine/node/commit/9984e082121f3e8d25750dd54c0d60c85cef0ac7))
|
||||
* **node:** bound the pre-walk CID resolve on its own short budget ([0d7711c](https://github.com/Twigpine/node/commit/0d7711c08917a70f982391249439485ddce382b0))
|
||||
* **node:** bound the request body the signature middleware buffers before it verifies ([#262](https://github.com/Twigpine/node/issues/262)) ([288ad92](https://github.com/Twigpine/node/commit/288ad92fe83526a717fa1a6ac37b1f3cf1f97961))
|
||||
* **node:** bring the Pinata pin path back into lockstep with the Kubo one ([4681f54](https://github.com/Twigpine/node/commit/4681f54327b58b6ebdd569a1cec17a3c2643ad13))
|
||||
* **node:** budget provenance and fallback walks separately ([ae96ffa](https://github.com/Twigpine/node/commit/ae96ffa46706a6d088a412aa02dbf2f715ceaa37))
|
||||
* **node:** canonicalize and 404-on-no-match in remove_label ([#344](https://github.com/Twigpine/node/issues/344)) ([#530](https://github.com/Twigpine/node/issues/530)) ([8c9e4ad](https://github.com/Twigpine/node/commit/8c9e4adc2d5ae3560c1bd3ba76c1d0ddb4de2af6))
|
||||
* **node:** carry discovery onto the per-(oid, repo) incompleteness marker ([bd0bef1](https://github.com/Twigpine/node/commit/bd0bef1476f90e99d9a697e2520c32b954f3ba6f))
|
||||
* **node:** charge discovery's probes against the per-run read budget ([bc20236](https://github.com/Twigpine/node/commit/bc202368e13202504fe77534273b508f7559a84a))
|
||||
* **node:** clamp every admission-held DB await in the CID resolver ([bad82b3](https://github.com/Twigpine/node/commit/bad82b3cd9fa970ebf67817395bfbd98591879b1))
|
||||
* **node:** close quarantine bypass on encrypted blobs, visibility routes, and sibling mutations ([#566](https://github.com/Twigpine/node/issues/566)) ([66fe646](https://github.com/Twigpine/node/commit/66fe6463200cce105eaee780c8e730b12598a630))
|
||||
* **node:** close review findings on the owner-push default ([2f0174c](https://github.com/Twigpine/node/commit/2f0174cc041fb6e9eb3c904172b37b5b9a6c094b))
|
||||
* **node:** close three host-class gaps in the public-URL gate ([#346](https://github.com/Twigpine/node/issues/346)) ([76ca04f](https://github.com/Twigpine/node/commit/76ca04f3552aa8c17305fca73fa1c3469a29903c))
|
||||
* **node:** count a completed fetch once per fetch, not per stateless-RPC POST ([1c690c1](https://github.com/Twigpine/node/commit/1c690c125c16ad2ed38a5885d0e16b96789b715e))
|
||||
* **node:** count fetch completion from the response outcome, not the request ([5a5d4e1](https://github.com/Twigpine/node/commit/5a5d4e1b43a5b37fcc57731ec504b221a58931f1))
|
||||
* **node:** drop withheld-subtree trees from the replication pin set ([#382](https://github.com/Twigpine/node/issues/382)) ([19a7ce2](https://github.com/Twigpine/node/commit/19a7ce2bbdad9dd96bdc35ed6230d14f772a5969))
|
||||
* **node:** durable post-receive outbox for receive-pack ([#26](https://github.com/Twigpine/node/issues/26) split 1/4) ([#558](https://github.com/Twigpine/node/issues/558)) ([f68d0dd](https://github.com/Twigpine/node/commit/f68d0dd3ffac587561499f63102cfd08732b72c2))
|
||||
* **node:** encrypt webhook HMAC secrets at rest and redact from Debug ([#575](https://github.com/Twigpine/node/issues/575)) ([4fc041a](https://github.com/Twigpine/node/commit/4fc041a3995965e199c5dc49d276717b40ab111a))
|
||||
* **node:** enforce owner-only push by default ([cdbda19](https://github.com/Twigpine/node/commit/cdbda19c29db576d2ea37ae74578c899b4eb3bb9))
|
||||
* **node:** Escape LIKE metacharacters in certificate prefix search ([eeafa1f](https://github.com/Twigpine/node/commit/eeafa1fa5350fb2d15ba30ac6cd6ad8247b4b7ec))
|
||||
* **node:** Escape LIKE wildcards in certificate prefix search ([96d8123](https://github.com/Twigpine/node/commit/96d8123e0f84e85a7b0234ad84f5e603a5a303aa))
|
||||
* **node:** fail pin_git_object when the add response carries no Hash ([#528](https://github.com/Twigpine/node/issues/528)) ([47c6e6c](https://github.com/Twigpine/node/commit/47c6e6ce2200dd196cb531bc6a6ed946cf587409))
|
||||
* **node:** fence a repo publish on the write attempt that owns it ([#285](https://github.com/Twigpine/node/issues/285)) ([ed76adf](https://github.com/Twigpine/node/commit/ed76adfe29d793229fbc63eeb86d24cac5d23d13))
|
||||
* **node:** gate /ipfs/pins and /arweave/anchors behind authentication ([#121](https://github.com/Twigpine/node/issues/121)) ([#543](https://github.com/Twigpine/node/issues/543)) ([d3845ce](https://github.com/Twigpine/node/commit/d3845ced69597b3e73e3c8cb5f452f6367897e5b))
|
||||
* **node:** gate agent-task reads behind visibility rules ([#553](https://github.com/Twigpine/node/issues/553)) ([4cd0a6a](https://github.com/Twigpine/node/commit/4cd0a6a306b8e131a87b5629d7a986d0e84b873f))
|
||||
* **node:** gate bounty mutations on repo read before status checks ([#529](https://github.com/Twigpine/node/issues/529)) ([8eedb93](https://github.com/Twigpine/node/commit/8eedb935c3525a91ac311e273c3f22d3b4eb4615))
|
||||
* **node:** gate GET /ipfs/{cid} tree objects so a withheld subtree's structure can't leak ([#135](https://github.com/Twigpine/node/issues/135)) ([21f8cb6](https://github.com/Twigpine/node/commit/21f8cb61350b2355edd2b2ed527d9fb9c4305bd9))
|
||||
* **node:** gate replica register/unregister on repo read visibility ([#445](https://github.com/Twigpine/node/issues/445)) ([0303222](https://github.com/Twigpine/node/commit/0303222eae486c6d519be1441d4b3b11115a05ab))
|
||||
* **node:** gate two Unix-only tests so the crate compiles on Windows ([5a7a77b](https://github.com/Twigpine/node/commit/5a7a77b8dc1723cde44b0439327e517e23a048ab)), closes [#228](https://github.com/Twigpine/node/issues/228)
|
||||
* **node:** give each discovery row its own slice of the pass budget ([22560b0](https://github.com/Twigpine/node/commit/22560b0872bd16dd66100681b1050faa677fc568))
|
||||
* **node:** harden peer readiness state ([6bd8f41](https://github.com/Twigpine/node/commit/6bd8f4121235ec6c4b7062aadcf3a631f174510c))
|
||||
* **node:** hold fork to the same repo-name rules as every other creation route ([#412](https://github.com/Twigpine/node/issues/412)) ([fc87586](https://github.com/Twigpine/node/commit/fc8758641147ca5f38561fc71b13b1e41a78a5ed))
|
||||
* **node:** honor pre-assigned task assignee on claim ([#550](https://github.com/Twigpine/node/issues/550)) ([a4026e8](https://github.com/Twigpine/node/commit/a4026e84ffe82f48b41817c8ba687fbc1aac8f9b))
|
||||
* **node:** implement reconciliation sweep as durability backstop ([#218](https://github.com/Twigpine/node/issues/218)) ([#551](https://github.com/Twigpine/node/issues/551)) ([a2262d9](https://github.com/Twigpine/node/commit/a2262d92b2029d7228e3bf8528aad4d7ed1865db))
|
||||
* **node:** make creation-route rate limit configurable; fix(gl): surface real errors on pr create failure ([#560](https://github.com/Twigpine/node/issues/560)) ([3c1ee27](https://github.com/Twigpine/node/commit/3c1ee27b60f7a9247f04c3144e8491a7c1f65f20))
|
||||
* **node:** make metrics::init race-safe with Once ([c45ec96](https://github.com/Twigpine/node/commit/c45ec9605d55171f7b8f1b0b542478097ddf43a3))
|
||||
* **node:** make the pin-source incompleteness marker per (object, repo) ([5575da4](https://github.com/Twigpine/node/commit/5575da4b65ff19da78a6b218da07f854706a811c))
|
||||
* **node:** map ipfs/arweave DB outages to 503 via bare ? ([#251](https://github.com/Twigpine/node/issues/251)) ([0e2328b](https://github.com/Twigpine/node/commit/0e2328bd8044ce6e8923cc6bd42219068d3f6b9d))
|
||||
* **node:** map ipfs/arweave DB outages to 503 via bare ? ([#251](https://github.com/Twigpine/node/issues/251)) ([c29a91a](https://github.com/Twigpine/node/commit/c29a91af6b7eb444e202f4724e3f677d2ef25466))
|
||||
* **node:** mint a continuation when a ceiling stops the /ipfs scan mid-page ([69cfbd4](https://github.com/Twigpine/node/commit/69cfbd4b3ccde243ff669392820f0fce86381663))
|
||||
* **node:** never hand back a continuation that made no progress ([76f6300](https://github.com/Twigpine/node/commit/76f630008f0088c952755b643126affdf962d12b))
|
||||
* **node:** opaque AppError::Internal and AppError::Db HTTP bodies ([#226](https://github.com/Twigpine/node/issues/226)) ([241b366](https://github.com/Twigpine/node/commit/241b36633bca7e0ead33bf670b7c1531c3bd9ab5))
|
||||
* **node:** opaque AppError::Internal and AppError::Db HTTP bodies ([#226](https://github.com/Twigpine/node/issues/226)) ([d192028](https://github.com/Twigpine/node/commit/d192028ee1487a3638c545d6a39e58a4575df1f1))
|
||||
* **node:** opaque GraphQL DB error messages ([#250](https://github.com/Twigpine/node/issues/250)) ([c10ccf1](https://github.com/Twigpine/node/commit/c10ccf1f059eae18fcb0b7058a35bb134838b790))
|
||||
* **node:** order the CID candidate list so it cannot depend on heap order ([366b599](https://github.com/Twigpine/node/commit/366b59933112b345113501a65f0f30e76518ff61))
|
||||
* **node:** owner-gate create_task repo_id against hosted repos ([#541](https://github.com/Twigpine/node/issues/541)) ([bac49f1](https://github.com/Twigpine/node/commit/bac49f150a2b99d7b14fc9d53e231b7e743a753c))
|
||||
* **node:** persist the libp2p identity instead of deriving it from the node DID ([#324](https://github.com/Twigpine/node/issues/324)) ([c301a68](https://github.com/Twigpine/node/commit/c301a681619e890a081c385d7cbab5b8a8d4efbb))
|
||||
* **node:** pin no-redirect and bound the body read on the iCaptcha public-key fetch ([#574](https://github.com/Twigpine/node/issues/574)) ([d15f194](https://github.com/Twigpine/node/commit/d15f194f7f69f80a7451a157df1a3d18a435c09e))
|
||||
* **node:** port discovery's candidate load onto the paged repo query ([4c665fb](https://github.com/Twigpine/node/commit/4c665fbca8641f7dfe6770d1ee5384a0af3d215c))
|
||||
* **node:** preserve legacy peer compatibility ([547f9ae](https://github.com/Twigpine/node/commit/547f9aed10706bc617d0e1e2e5346cf52d19a901))
|
||||
* **node:** reconcile public-read messaging with per-repo enforcement ([#439](https://github.com/Twigpine/node/issues/439)) ([0366f18](https://github.com/Twigpine/node/commit/0366f18458dd996ea18057ce60abf88673e16ed4))
|
||||
* **node:** record a discovered source and its marker in one commit ([9babb5e](https://github.com/Twigpine/node/commit/9babb5ece053608c522f49b4528de23d5b61b749))
|
||||
* **node:** reject revision syntax in issue ids and drop service passthrough ([#521](https://github.com/Twigpine/node/issues/521)) ([421f03a](https://github.com/Twigpine/node/commit/421f03ac46592c57bc0df5a8fa79061c74d32fe6))
|
||||
* **node:** repair source-less legacy pins by bounded additive discovery ([d88c0c7](https://github.com/Twigpine/node/commit/d88c0c726c8630e104e0161366d1815d69e96428))
|
||||
* **node:** require Content-Digest on a signed request ([#306](https://github.com/Twigpine/node/issues/306)) ([189aad9](https://github.com/Twigpine/node/commit/189aad96e5c831f94cef654048d2d9797a5f574d))
|
||||
* **node:** resolve issue ids from git refnames, not request input ([#526](https://github.com/Twigpine/node/issues/526)) ([3063719](https://github.com/Twigpine/node/commit/30637193a626da800984363b6192fc50131b9741))
|
||||
* **node:** resume the /ipfs scan per candidate so a starved one is not skipped ([d0635d9](https://github.com/Twigpine/node/commit/d0635d95b7d601490474419c6bdb99b0ce402e71))
|
||||
* **node:** rewind the pin repair sweep cursor on every completed walk ([1b44b12](https://github.com/Twigpine/node/commit/1b44b12b468c265f946390a57cd815d67d205166))
|
||||
* **node:** rotate discovery candidates across traversals ([4fdfa1c](https://github.com/Twigpine/node/commit/4fdfa1cedb3dcb6d7d6cc4004e7277a9fa95de94))
|
||||
* **node:** scope bounty aggregates to anonymously-listable repos ([#477](https://github.com/Twigpine/node/issues/477)) ([#527](https://github.com/Twigpine/node/issues/527)) ([317a155](https://github.com/Twigpine/node/commit/317a1550c2810932118429b9987ea437a1465cf5))
|
||||
* **node:** scope the refs listing by the canonical repo slug ([#388](https://github.com/Twigpine/node/issues/388)) ([131bb95](https://github.com/Twigpine/node/commit/131bb95c686698d0f6a90dabd3976509df86b595))
|
||||
* **node:** size the ipfs work floor for provenance walks plus one full search ([dc5daa1](https://github.com/Twigpine/node/commit/dc5daa14b4d0816d8a14fabdf057259c1d177a46))
|
||||
* **node:** stop reporting a Kubo pin whose DB record never landed ([f839fb9](https://github.com/Twigpine/node/commit/f839fb924653f1b012c2b34d51260ea3faba7f62))
|
||||
* **node:** stop the /ipfs size probe reporting a fault as a missing object ([ed6355e](https://github.com/Twigpine/node/commit/ed6355ed2c42bed09dec35d46904805e97de806c))
|
||||
* **node:** use readiness for peer liveness ([225644a](https://github.com/Twigpine/node/commit/225644a3ea0302ae91959016928df1040ddd9cb5))
|
||||
* **node:** use readiness for peer liveness ([1a841a9](https://github.com/Twigpine/node/commit/1a841a99e2d40129a33f1898d3ce28d05eb04632))
|
||||
* **node:** validate git branch refs to close option injection ([#379](https://github.com/Twigpine/node/issues/379)) ([6233bd9](https://github.com/Twigpine/node/commit/6233bd9d850b60fdd2fb05afaa3d996d9f37f59d))
|
||||
* **node:** validate ref-update SHAs are hex, not just length 40 ([#433](https://github.com/Twigpine/node/issues/433)) ([911be4d](https://github.com/Twigpine/node/commit/911be4d9fef309e56117440d21e5dad7718be666))
|
||||
* **node:** version ref_certificates for forward compat ([#26](https://github.com/Twigpine/node/issues/26) split 3/4) ([#548](https://github.com/Twigpine/node/issues/548)) ([38bab41](https://github.com/Twigpine/node/commit/38bab4191baa5e8778f3412dc1f013d1f654bb15))
|
||||
* **release:** bump icaptcha-client with the rest of the workspace ([6668525](https://github.com/Twigpine/node/commit/6668525f41936ed2e1f8a1334b0dd17eaea866f0))
|
||||
* **repo_store:** use SHA-256 for stable advisory-lock key ([#210](https://github.com/Twigpine/node/issues/210)) ([c380f9c](https://github.com/Twigpine/node/commit/c380f9cee5139a2cab6fc52000d43844b86bb143))
|
||||
* **repo_store:** use SHA-256 for stable advisory-lock key ([#210](https://github.com/Twigpine/node/issues/210)) ([89e10d0](https://github.com/Twigpine/node/commit/89e10d0ff12dcccae3bd4490548d906f77d76636))
|
||||
* **review:** assert the read-timeout postcondition and drop the socket clone ([4d9bd5a](https://github.com/Twigpine/node/commit/4d9bd5a18a0692baa67f94aac8e8663d8e69bdac))
|
||||
* **review:** bound the probe's channel waits and stop overstating the handshake ([d4e5ecf](https://github.com/Twigpine/node/commit/d4e5ecf299afaabf05eee22db462b9314b5a6998))
|
||||
* **review:** bound token key, rule bytes, and the discovery load ([e6cfb58](https://github.com/Twigpine/node/commit/e6cfb588c2a1600d59f2af88471d248a99532f03))
|
||||
* **review:** keep the resume token on every terminal that still holds one ([8c39cfd](https://github.com/Twigpine/node/commit/8c39cfdcb347ca824cbec7d6b56720df9517a472))
|
||||
* **review:** make the coupled sites findable and pin two more properties ([a7e8e35](https://github.com/Twigpine/node/commit/a7e8e35030cae4e1126cda6c161bc516e3b8ffff))
|
||||
* **review:** make the LIKE escape independent of the SQL parser mode ([1d3987b](https://github.com/Twigpine/node/commit/1d3987bba6619a64a2cef15023224cbaa6cc8d1b)), closes [#319](https://github.com/Twigpine/node/issues/319)
|
||||
* **review:** mint a continuation at every ceiling and widen the token ([342dd48](https://github.com/Twigpine/node/commit/342dd48d2426a8870423cd537781b52d03e8f175))
|
||||
* **test:** match SHA-256 advisory-lock key in receive-pack deadline test ([c68e1b0](https://github.com/Twigpine/node/commit/c68e1b07f8db752cff786bdc9c226b49889a40b1))
|
||||
* **whoami:** update registered status handling on 404 response ([75b0df8](https://github.com/Twigpine/node/commit/75b0df8f1c7bda6a02444ce17e11088b839a51b6))
|
||||
|
||||
## [0.7.1](https://github.com/Gitlawb/node/compare/v0.7.0...v0.7.1) (2026-08-03)
|
||||
|
||||
|
||||
|
||||
Generated
+6
-6
@@ -3470,7 +3470,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "git-remote-gitlawb"
|
||||
version = "0.8.0"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -3488,7 +3488,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gitlawb-attest"
|
||||
version = "0.8.0"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"ed25519-dalek 2.2.0",
|
||||
@@ -3505,7 +3505,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gitlawb-core"
|
||||
version = "0.8.0"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
@@ -3533,7 +3533,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gitlawb-node"
|
||||
version = "0.8.0"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"alloy",
|
||||
"anyhow",
|
||||
@@ -3594,7 +3594,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gl"
|
||||
version = "0.8.0"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"alloy",
|
||||
"anyhow",
|
||||
@@ -4062,7 +4062,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icaptcha-client"
|
||||
version = "0.8.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gitlawb-core",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "git-remote-gitlawb"
|
||||
description = "Git remote helper — enables 'git clone gitlawb://did:gitlawb:...'"
|
||||
version = "0.8.0" # x-release-please-version
|
||||
version = "0.7.1" # x-release-please-version
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "gitlawb-attest"
|
||||
description = "External Attestation v1: pluggable provenance attachments for Twigpine ref-update certs"
|
||||
version = "0.8.0" # x-release-please-version
|
||||
version = "0.7.1" # x-release-please-version
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "gitlawb-core"
|
||||
description = "Core cryptographic primitives for the Twigpine network: DIDs, CIDs, UCAN, HTTP Signatures, ref-update certificates"
|
||||
version = "0.8.0" # x-release-please-version
|
||||
version = "0.7.1" # x-release-please-version
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "gitlawb-node"
|
||||
description = "The Twigpine node daemon — git hosting over HTTP with DID auth"
|
||||
version = "0.8.0" # x-release-please-version
|
||||
version = "0.7.1" # x-release-please-version
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -439,6 +439,7 @@ pub async fn trigger_sync(State(state): State<AppState>) -> Result<Json<serde_js
|
||||
"refs/heads/main",
|
||||
"0000000000000000000000000000000000000000",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
enqueued += 1;
|
||||
@@ -520,7 +521,14 @@ pub async fn notify_sync(
|
||||
|
||||
state
|
||||
.db
|
||||
.enqueue_sync(&req.repo, &req.node_did, &req.ref_name, &req.new_sha, None)
|
||||
.enqueue_sync(
|
||||
&req.repo,
|
||||
&req.node_did,
|
||||
&req.ref_name,
|
||||
&req.new_sha,
|
||||
None,
|
||||
req.timestamp.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Mirror the gossipsub-receive handler: insert the same record we'd
|
||||
@@ -1282,6 +1290,37 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn sync_notify_preserves_origin_timestamp_for_sync_lag(pool: PgPool) {
|
||||
let state = test_state(pool).await;
|
||||
let db = state.db.clone();
|
||||
let peer_did = seed_peer(&state).await;
|
||||
let timestamp = "2026-08-16T00:00:00Z";
|
||||
let body = serde_json::json!({
|
||||
"repo": "z6Mkfoo/timestamped",
|
||||
"ref_name": "refs/heads/main",
|
||||
"new_sha": "0000000000000000000000000000000000000000",
|
||||
"node_did": peer_did,
|
||||
"timestamp": timestamp,
|
||||
})
|
||||
.to_string();
|
||||
let router = crate::server::build_router(state);
|
||||
|
||||
let resp = router
|
||||
.oneshot(unsigned_post(
|
||||
"/api/v1/sync/notify",
|
||||
&body,
|
||||
"198.51.100.23:5000",
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let queued = db.dequeue_pending_syncs(100).await.unwrap();
|
||||
assert_eq!(queued.len(), 1);
|
||||
assert_eq!(queued[0].origin_timestamp.as_deref(), Some(timestamp));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn sync_notify_unknown_peer_wins_over_malformed_slug(pool: PgPool) {
|
||||
// Ordering: the slug check sits after the peer gate, so an unknown peer
|
||||
|
||||
@@ -1777,6 +1777,7 @@ pub async fn git_upload_pack(
|
||||
headers: axum::http::HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Result<Response> {
|
||||
let fetch_started = std::time::Instant::now();
|
||||
// #62 cheap load shed. Permit-less snapshot, not admission; see git_info_refs for
|
||||
// what it does and does not bound. The authoritative hold is `git_permit` below,
|
||||
// after the per-source cap.
|
||||
@@ -1979,6 +1980,7 @@ pub async fn git_upload_pack(
|
||||
// that mislabel predates this change and is tracked as a follow-up.
|
||||
if should_count_fetch(finalizes_fetch, served_filtered_pack, served_pack) {
|
||||
crate::metrics::record_fetch(&format!("{owner}/{name}"));
|
||||
crate::metrics::observe_fetch_duration(fetch_started.elapsed());
|
||||
crate::metrics::observe_pack_size(body_len as f64);
|
||||
}
|
||||
Ok(resp)
|
||||
@@ -2190,6 +2192,7 @@ async fn notify_peer_of_ref(
|
||||
node_did: &str,
|
||||
pusher_did: &str,
|
||||
owner_did: &str,
|
||||
origin_timestamp: &str,
|
||||
) {
|
||||
let body = serde_json::json!({
|
||||
"repo": repo_slug,
|
||||
@@ -2198,7 +2201,7 @@ async fn notify_peer_of_ref(
|
||||
"node_did": node_did,
|
||||
"pusher_did": pusher_did,
|
||||
"old_sha": old_sha,
|
||||
"timestamp": chrono::Utc::now().to_rfc3339(),
|
||||
"timestamp": origin_timestamp,
|
||||
"owner_did": owner_did,
|
||||
});
|
||||
let body_bytes = match serde_json::to_vec(&body) {
|
||||
@@ -2248,6 +2251,7 @@ async fn notify_peer_of_refs(
|
||||
node_did: &str,
|
||||
pusher_did: &str,
|
||||
owner_did: &str,
|
||||
origin_timestamp: &str,
|
||||
) {
|
||||
for (ref_name, old_sha, new_sha) in ref_updates {
|
||||
notify_peer_of_ref(
|
||||
@@ -2262,6 +2266,7 @@ async fn notify_peer_of_refs(
|
||||
node_did,
|
||||
pusher_did,
|
||||
owner_did,
|
||||
origin_timestamp,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -2280,6 +2285,7 @@ pub async fn git_receive_pack(
|
||||
headers: axum::http::HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Result<Response> {
|
||||
let push_started = std::time::Instant::now();
|
||||
let name = smart_http_repo_name(&repo)?;
|
||||
// Fast-path shed before the DB lookup when the write pool is ALREADY saturated, so a
|
||||
// push flood against a full pool does not hit Postgres per request. Best-effort
|
||||
@@ -3399,6 +3405,7 @@ pub async fn git_receive_pack(
|
||||
// that case via `run_effects`, so this is defensive).
|
||||
let _ = state.db.touch_repo(&record.id).await;
|
||||
crate::metrics::record_push(&record.id);
|
||||
crate::metrics::observe_push_duration(push_started.elapsed());
|
||||
crate::metrics::observe_pack_size(body_len as f64);
|
||||
|
||||
// Claim ownership before inline effects: a background worker may
|
||||
@@ -3698,6 +3705,11 @@ pub(crate) async fn post_receive_replication_tail(
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture this before the asynchronous replication work below. The same
|
||||
// origin time travels over gossip and HTTP so receiving peers can measure
|
||||
// push-to-visible lag rather than notification-to-visible lag.
|
||||
let origin_timestamp = Utc::now().to_rfc3339();
|
||||
|
||||
// Replication enforcement (Phase 2): decide once per push whether the public
|
||||
// may read this repo at all and, if so, which blob OIDs must not leave the
|
||||
// node. `withheld == None` means this push pins nothing (private / mode A /
|
||||
@@ -3912,6 +3924,7 @@ pub(crate) async fn post_receive_replication_tail(
|
||||
let owner_did_for_arweave = record.owner_did.clone();
|
||||
let self_public_url = state.config.public_url.clone();
|
||||
let node_keypair = Arc::clone(&state.node_keypair);
|
||||
let origin_timestamp_for_announce = origin_timestamp.clone();
|
||||
// #174 F2a: gated on the cheap announce predicate, not on `withheld`.
|
||||
// `withheld` is None for a push that coalesced (it never walked), and
|
||||
// this task's work is per-push and non-idempotent, so keying it on the
|
||||
@@ -4036,7 +4049,7 @@ pub(crate) async fn post_receive_replication_tail(
|
||||
ref_name: ref_name.clone(),
|
||||
old_sha: old_sha.clone(),
|
||||
new_sha: new_sha.clone(),
|
||||
timestamp: chrono::Utc::now().to_rfc3339(),
|
||||
timestamp: origin_timestamp_for_announce.clone(),
|
||||
cert_id: None,
|
||||
cid: cid.map(|s| s.to_string()),
|
||||
sig: None,
|
||||
@@ -4139,6 +4152,7 @@ pub(crate) async fn post_receive_replication_tail(
|
||||
&node_did_str,
|
||||
&pusher_did_clone,
|
||||
&record.owner_did,
|
||||
&origin_timestamp_for_announce,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -8062,6 +8076,9 @@ mod tests {
|
||||
mockito::Matcher::PartialJsonString(
|
||||
r#"{"owner_did":"did:key:zOwner"}"#.to_string(),
|
||||
),
|
||||
mockito::Matcher::PartialJsonString(
|
||||
r#"{"timestamp":"2026-08-16T00:00:00Z"}"#.to_string(),
|
||||
),
|
||||
]))
|
||||
.with_status(200)
|
||||
.expect(1)
|
||||
@@ -8098,6 +8115,7 @@ mod tests {
|
||||
"did:key:zNode",
|
||||
"did:key:zPusher",
|
||||
"did:key:zOwner",
|
||||
"2026-08-16T00:00:00Z",
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -8146,6 +8164,7 @@ mod tests {
|
||||
"did:key:zNode",
|
||||
"did:key:zPusher",
|
||||
"did:key:zOwner",
|
||||
"2026-08-16T00:00:00Z",
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -2372,6 +2372,16 @@ const MIGRATIONS: &[Migration] = &[
|
||||
"CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_pending ON webhook_deliveries (sent_at, created_at)",
|
||||
],
|
||||
},
|
||||
Migration {
|
||||
version: 47,
|
||||
name: "sync_queue_origin_timestamp",
|
||||
stmts: &[
|
||||
// The source timestamp accompanies a ref update across either
|
||||
// transport. It remains nullable for rows from older peers and
|
||||
// manually-triggered syncs, which must not report a fabricated lag.
|
||||
"ALTER TABLE sync_queue ADD COLUMN IF NOT EXISTS origin_timestamp TEXT",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/// Max distinct source repos recorded per pinned object (F1, #173 jatmn round 8).
|
||||
@@ -3532,6 +3542,7 @@ pub struct SyncQueueItem {
|
||||
pub ref_name: String,
|
||||
pub new_sha: String,
|
||||
pub cid: Option<String>,
|
||||
pub origin_timestamp: Option<String>,
|
||||
pub status: String,
|
||||
pub enqueued_at: String,
|
||||
}
|
||||
@@ -3544,10 +3555,11 @@ impl Db {
|
||||
ref_name: &str,
|
||||
new_sha: &str,
|
||||
cid: Option<&str>,
|
||||
origin_timestamp: Option<&str>,
|
||||
) -> Result<()> {
|
||||
sqlx::query(
|
||||
"INSERT INTO sync_queue (id, repo, node_did, ref_name, new_sha, cid, status, enqueued_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'pending', $7)
|
||||
"INSERT INTO sync_queue (id, repo, node_did, ref_name, new_sha, cid, origin_timestamp, status, enqueued_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending', $8)
|
||||
ON CONFLICT DO NOTHING",
|
||||
)
|
||||
.bind(Uuid::new_v4().to_string())
|
||||
@@ -3556,6 +3568,7 @@ impl Db {
|
||||
.bind(ref_name)
|
||||
.bind(new_sha)
|
||||
.bind(cid)
|
||||
.bind(origin_timestamp)
|
||||
.bind(Utc::now().to_rfc3339())
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
@@ -3598,7 +3611,7 @@ impl Db {
|
||||
SELECT id FROM sync_queue WHERE status = 'pending'
|
||||
ORDER BY COALESCE(attempted_at, enqueued_at) ASC LIMIT $1
|
||||
)
|
||||
RETURNING id, repo, node_did, ref_name, new_sha, cid, status, enqueued_at",
|
||||
RETURNING id, repo, node_did, ref_name, new_sha, cid, origin_timestamp, status, enqueued_at",
|
||||
)
|
||||
.bind(limit)
|
||||
.bind(Utc::now().to_rfc3339())
|
||||
@@ -3613,6 +3626,7 @@ impl Db {
|
||||
ref_name: r.get("ref_name"),
|
||||
new_sha: r.get("new_sha"),
|
||||
cid: r.get("cid"),
|
||||
origin_timestamp: r.get("origin_timestamp"),
|
||||
status: r.get("status"),
|
||||
enqueued_at: r.get("enqueued_at"),
|
||||
})
|
||||
@@ -10454,7 +10468,7 @@ mod migration_tests {
|
||||
db.migrate().await.unwrap();
|
||||
}
|
||||
|
||||
// ── sync_queue scheduling (attempted_at, v17) ────────────────────────────
|
||||
// ── sync_queue scheduling and origin timestamps (v17/v18) ───────────────
|
||||
|
||||
async fn enqueue_one(db: &super::Db, repo: &str) {
|
||||
db.enqueue_sync(
|
||||
@@ -10463,6 +10477,7 @@ mod migration_tests {
|
||||
"refs/heads/main",
|
||||
&"0".repeat(40),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -10532,6 +10547,72 @@ mod migration_tests {
|
||||
db.migrate().await.unwrap();
|
||||
}
|
||||
|
||||
/// Upgrade-path test for the nullable source timestamp. The worker must
|
||||
/// preserve it when it exists, while rows written before v18 remain valid.
|
||||
#[sqlx::test]
|
||||
async fn migration_v47_adds_sync_queue_origin_timestamp(pool: sqlx::PgPool) {
|
||||
let db = super::Db::for_testing(pool);
|
||||
db.migrate().await.unwrap();
|
||||
|
||||
sqlx::query("ALTER TABLE sync_queue DROP COLUMN origin_timestamp")
|
||||
.execute(&db.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query("DELETE FROM schema_migrations WHERE version = 47")
|
||||
.execute(&db.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO sync_queue (id, repo, node_did, ref_name, new_sha, status, enqueued_at)
|
||||
VALUES ('legacy-sync', 'z6Mkfoo/legacy', 'did:key:zPEER', 'refs/heads/main', $1, 'pending', $2)",
|
||||
)
|
||||
.bind("0".repeat(40))
|
||||
.bind("2026-08-16T00:00:00Z")
|
||||
.execute(&db.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.migrate().await.unwrap();
|
||||
|
||||
let col: (String, String) = sqlx::query_as(
|
||||
"SELECT data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'sync_queue' AND column_name = 'origin_timestamp'",
|
||||
)
|
||||
.fetch_one(&db.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(col.0, "text");
|
||||
assert_eq!(col.1, "YES", "origin_timestamp must be nullable");
|
||||
|
||||
let items = db.dequeue_pending_syncs(10).await.unwrap();
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0].origin_timestamp, None);
|
||||
db.migrate().await.unwrap();
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn dequeue_preserves_sync_origin_timestamp(pool: sqlx::PgPool) {
|
||||
let db = super::Db::for_testing(pool);
|
||||
db.migrate().await.unwrap();
|
||||
let timestamp = "2026-08-16T00:00:00Z";
|
||||
db.enqueue_sync(
|
||||
"z6Mkfoo/timestamped",
|
||||
"did:key:zPEER",
|
||||
"refs/heads/main",
|
||||
&"0".repeat(40),
|
||||
None,
|
||||
Some(timestamp),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let items = db.dequeue_pending_syncs(10).await.unwrap();
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0].origin_timestamp.as_deref(), Some(timestamp));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn migration_v27_drops_raw_assignee_index(pool: sqlx::PgPool) {
|
||||
let db = super::Db::for_testing(pool);
|
||||
@@ -14072,7 +14153,7 @@ mod ref_certificate_tests {
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query("DELETE FROM schema_migrations WHERE version = 18")
|
||||
sqlx::query("DELETE FROM schema_migrations WHERE version = 47")
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
//!
|
||||
//! * is push traffic flowing? — `gitlawb_pushes_total`
|
||||
//! * is fetch traffic flowing? — `gitlawb_fetches_total`
|
||||
//! * how long do successful push and fetch operations take? —
|
||||
//! `gitlawb_push_duration_seconds` / `gitlawb_fetch_duration_seconds`
|
||||
//! * are signature checks passing or failing? —
|
||||
//! `gitlawb_auth_successes_total` / `gitlawb_auth_failures_total`
|
||||
//! * is the sync worker making progress? —
|
||||
@@ -18,6 +20,8 @@
|
||||
//! `gitlawb_gossip_replay_guard_saturated_total`
|
||||
//! * how big are the packs we're sending and receiving? —
|
||||
//! `gitlawb_pack_size_bytes`
|
||||
//! * how long until an origin ref update is visible on this mirror? —
|
||||
//! `gitlawb_sync_lag_seconds`
|
||||
//! * a single `gitlawb_info{version, did}` gauge = 1, for joins/dashboards
|
||||
//! * currently-connected peer count — `gitlawb_peers_connected`
|
||||
//! * reconciliation sweep gaps found and filled —
|
||||
@@ -34,11 +38,10 @@
|
||||
//! free.
|
||||
//!
|
||||
//! Follow-ups (not in this module):
|
||||
//! * per-route latency histograms (TraceLayer already gives us spans)
|
||||
//! * per-peer p2p counters
|
||||
//! * ipfs / pinata counters
|
||||
|
||||
use std::sync::OnceLock;
|
||||
use std::{sync::OnceLock, time::Duration};
|
||||
|
||||
use prometheus::{
|
||||
Encoder, Histogram, HistogramOpts, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Opts,
|
||||
@@ -60,6 +63,9 @@ static WEBHOOK_DELIVERIES: OnceLock<IntCounterVec> = OnceLock::new();
|
||||
static GOSSIP_INGEST: OnceLock<IntCounterVec> = OnceLock::new();
|
||||
static GOSSIP_REPLAY_GUARD_SATURATED: OnceLock<IntCounter> = OnceLock::new();
|
||||
static PACK_SIZE: OnceLock<Histogram> = OnceLock::new();
|
||||
static PUSH_DURATION: OnceLock<Histogram> = OnceLock::new();
|
||||
static FETCH_DURATION: OnceLock<Histogram> = OnceLock::new();
|
||||
static SYNC_LAG: OnceLock<Histogram> = OnceLock::new();
|
||||
static PEERS_CONNECTED: OnceLock<IntGauge> = OnceLock::new();
|
||||
static RECONCILIATION_GAPS_FOUND: OnceLock<IntCounter> = OnceLock::new();
|
||||
static RECONCILIATION_GAPS_FILLED: OnceLock<IntCounter> = OnceLock::new();
|
||||
@@ -125,6 +131,51 @@ fn init_inner(version: &str, node_did: &str) {
|
||||
.expect("register gitlawb_fetches_total");
|
||||
FETCHES.set(fetches).expect("set FETCHES once");
|
||||
|
||||
let push_duration = Histogram::with_opts(
|
||||
HistogramOpts::new(
|
||||
"gitlawb_push_duration_seconds",
|
||||
"Duration of successful git push (receive-pack) handlers in seconds",
|
||||
)
|
||||
.buckets(operation_duration_buckets()),
|
||||
)
|
||||
.expect("gitlawb_push_duration_seconds definition");
|
||||
registry
|
||||
.register(Box::new(push_duration.clone()))
|
||||
.expect("register gitlawb_push_duration_seconds");
|
||||
PUSH_DURATION
|
||||
.set(push_duration)
|
||||
.expect("set PUSH_DURATION once");
|
||||
|
||||
let fetch_duration = Histogram::with_opts(
|
||||
HistogramOpts::new(
|
||||
"gitlawb_fetch_duration_seconds",
|
||||
"Duration of successful git fetch (upload-pack) completion requests in seconds",
|
||||
)
|
||||
.buckets(operation_duration_buckets()),
|
||||
)
|
||||
.expect("gitlawb_fetch_duration_seconds definition");
|
||||
registry
|
||||
.register(Box::new(fetch_duration.clone()))
|
||||
.expect("register gitlawb_fetch_duration_seconds");
|
||||
FETCH_DURATION
|
||||
.set(fetch_duration)
|
||||
.expect("set FETCH_DURATION once");
|
||||
|
||||
let sync_lag = Histogram::with_opts(
|
||||
HistogramOpts::new(
|
||||
"gitlawb_sync_lag_seconds",
|
||||
"Lag from an origin ref-update timestamp to successful local mirror visibility in seconds",
|
||||
)
|
||||
.buckets(vec![
|
||||
1.0, 5.0, 10.0, 30.0, 60.0, 120.0, 300.0, 600.0, 1_800.0, 3_600.0,
|
||||
]),
|
||||
)
|
||||
.expect("gitlawb_sync_lag_seconds definition");
|
||||
registry
|
||||
.register(Box::new(sync_lag.clone()))
|
||||
.expect("register gitlawb_sync_lag_seconds");
|
||||
SYNC_LAG.set(sync_lag).expect("set SYNC_LAG once");
|
||||
|
||||
let auth_successes = IntCounterVec::new(
|
||||
Opts::new(
|
||||
"gitlawb_auth_successes_total",
|
||||
@@ -302,6 +353,27 @@ pub fn record_fetch(repo: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Observe the elapsed time of a successful git push handler.
|
||||
pub fn observe_push_duration(duration: Duration) {
|
||||
observe_duration(&PUSH_DURATION, duration);
|
||||
}
|
||||
|
||||
/// Observe the elapsed time of a successful git fetch completion request.
|
||||
pub fn observe_fetch_duration(duration: Duration) {
|
||||
observe_duration(&FETCH_DURATION, duration);
|
||||
}
|
||||
|
||||
/// Observe end-to-end lag until an origin ref update is visible on this mirror.
|
||||
pub fn observe_sync_lag(duration: Duration) {
|
||||
observe_duration(&SYNC_LAG, duration);
|
||||
}
|
||||
|
||||
fn observe_duration(histogram: &OnceLock<Histogram>, duration: Duration) {
|
||||
if let Some(h) = histogram.get() {
|
||||
h.observe(duration.as_secs_f64());
|
||||
}
|
||||
}
|
||||
|
||||
/// Test-only: current `gitlawb_fetches_total` value for `repo` (0 if the registry
|
||||
/// is not initialized). Lets api-layer tests assert the completed-fetch count with
|
||||
/// a unique label instead of scraping the encoded text.
|
||||
@@ -420,6 +492,12 @@ pub fn observe_pack_size(bytes: f64) {
|
||||
}
|
||||
}
|
||||
|
||||
fn operation_duration_buckets() -> Vec<f64> {
|
||||
vec![
|
||||
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0, 120.0, 300.0,
|
||||
]
|
||||
}
|
||||
|
||||
/// Update the currently-connected peer count gauge.
|
||||
pub fn set_peers_connected(count: i64) {
|
||||
if let Some(g) = PEERS_CONNECTED.get() {
|
||||
@@ -485,6 +563,9 @@ mod tests {
|
||||
.expect("PUSHES set after init")
|
||||
.with_label_values(&["alice/repo"])
|
||||
.inc();
|
||||
observe_push_duration(Duration::from_millis(42));
|
||||
observe_fetch_duration(Duration::from_millis(24));
|
||||
observe_sync_lag(Duration::from_secs(12));
|
||||
record_reconciliation_gaps_found(7);
|
||||
record_reconciliation_gaps_filled(3);
|
||||
|
||||
@@ -501,6 +582,16 @@ mod tests {
|
||||
body.contains("gitlawb_pushes_total{repo=\"alice/repo\"} 1"),
|
||||
"expected the incremented counter to be visible in: {body}"
|
||||
);
|
||||
for metric in [
|
||||
"gitlawb_push_duration_seconds",
|
||||
"gitlawb_fetch_duration_seconds",
|
||||
"gitlawb_sync_lag_seconds",
|
||||
] {
|
||||
assert!(
|
||||
body.contains(&format!("# TYPE {metric} histogram")),
|
||||
"expected {metric} histogram in: {body}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
body.contains("gitlawb_reconciliation_gaps_found_total 7"),
|
||||
"expected the reconciliation gaps-found counter to be visible in: {body}"
|
||||
@@ -546,6 +637,9 @@ mod tests {
|
||||
// no diagnostic value.
|
||||
record_gossip_ingest("accepted");
|
||||
observe_pack_size(1024.0);
|
||||
observe_push_duration(Duration::from_secs(1));
|
||||
observe_fetch_duration(Duration::from_secs(1));
|
||||
observe_sync_lag(Duration::from_secs(1));
|
||||
set_peers_connected(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -1498,6 +1498,7 @@ pub(crate) async fn ingest_ref_update(
|
||||
&event.ref_name,
|
||||
&event.new_sha,
|
||||
event.cid.as_deref(),
|
||||
Some(event.timestamp.as_str()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -16,7 +16,9 @@ use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use gitlawb_core::identity::Keypair;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
@@ -180,6 +182,27 @@ fn resolve_origin_url(peers: &[crate::db::PeerRecord], node_did: &str) -> Option
|
||||
.map(|p| p.http_url.trim_end_matches('/').to_string())
|
||||
}
|
||||
|
||||
/// Convert a sender-supplied RFC-3339 ref-update timestamp into an observable
|
||||
/// lag. Missing, malformed, and future timestamps are deliberately ignored:
|
||||
/// older peers and clock-skewed peers must not create fabricated observations.
|
||||
const MAX_SYNC_LAG_OBSERVATION: Duration = Duration::from_secs(86_400);
|
||||
|
||||
fn sync_lag_from_origin_timestamp(
|
||||
origin_timestamp: Option<&str>,
|
||||
visible_at: DateTime<Utc>,
|
||||
) -> Option<Duration> {
|
||||
let origin = DateTime::parse_from_rfc3339(origin_timestamp?).ok()?;
|
||||
let lag = visible_at
|
||||
.signed_duration_since(origin.with_timezone(&Utc))
|
||||
.to_std()
|
||||
.ok()?;
|
||||
// The origin timestamp is unauthenticated input from a known-but-not-proofed
|
||||
// peer: an absurd age (a 1970 date, a stale clock) would park a fabricated
|
||||
// observation in the histogram's tail buckets, so past a day the value is
|
||||
// ignored rather than sampled.
|
||||
(lag <= MAX_SYNC_LAG_OBSERVATION).then_some(lag)
|
||||
}
|
||||
|
||||
async fn process_batch(
|
||||
db: &Db,
|
||||
config: &Config,
|
||||
@@ -396,7 +419,7 @@ async fn process_batch(
|
||||
false
|
||||
};
|
||||
// Register in DB so git smart HTTP can serve the mirrored repo
|
||||
let _ = db
|
||||
let mirror_visible = db
|
||||
.upsert_mirror_repo(
|
||||
owner_short,
|
||||
repo_name,
|
||||
@@ -404,7 +427,19 @@ async fn process_batch(
|
||||
machine_id,
|
||||
quarantined,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.is_ok();
|
||||
// The lag is push-to-visible: sample at registration, before
|
||||
// the encrypted-blob tail and mark_sync_done. A quarantined
|
||||
// mirror is admitted but withheld from readers, so it reports
|
||||
// no visibility lag.
|
||||
if mirror_visible && !quarantined {
|
||||
if let Some(lag) =
|
||||
sync_lag_from_origin_timestamp(item.origin_timestamp.as_deref(), Utc::now())
|
||||
{
|
||||
crate::metrics::observe_sync_lag(lag);
|
||||
}
|
||||
}
|
||||
// Option B2: carry the encrypted withheld-blob envelopes too, so an
|
||||
// authorized reader can recover private content from this mirror if
|
||||
// the origin dies. `item.repo` is the slug "{owner_short}/{name}",
|
||||
@@ -1582,11 +1617,32 @@ mod tests {
|
||||
}
|
||||
|
||||
async fn enqueue(db: &Db, repo: &str, did: &str) {
|
||||
db.enqueue_sync(repo, did, "refs/heads/main", &"0".repeat(40), None)
|
||||
db.enqueue_sync(repo, did, "refs/heads/main", &"0".repeat(40), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_lag_uses_origin_timestamp_only_when_it_is_in_the_past() {
|
||||
let visible_at = chrono::DateTime::parse_from_rfc3339("2026-08-16T00:00:12Z")
|
||||
.unwrap()
|
||||
.with_timezone(&chrono::Utc);
|
||||
|
||||
assert_eq!(
|
||||
sync_lag_from_origin_timestamp(Some("2026-08-16T00:00:00Z"), visible_at),
|
||||
Some(Duration::from_secs(12))
|
||||
);
|
||||
assert_eq!(sync_lag_from_origin_timestamp(None, visible_at), None);
|
||||
assert_eq!(
|
||||
sync_lag_from_origin_timestamp(Some("not-a-timestamp"), visible_at),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
sync_lag_from_origin_timestamp(Some("2026-08-16T00:00:13Z"), visible_at),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
fn dir_entries(dir: &Path) -> Vec<String> {
|
||||
let mut names: Vec<String> = std::fs::read_dir(dir)
|
||||
.unwrap()
|
||||
@@ -2808,6 +2864,7 @@ mod tests {
|
||||
cid: None,
|
||||
status: "pending".to_string(),
|
||||
enqueued_at: chrono::Utc::now().to_rfc3339(),
|
||||
origin_timestamp: None,
|
||||
};
|
||||
async fn status_of(pool: &sqlx::PgPool, id: &str) -> String {
|
||||
let row: (String,) = sqlx::query_as("SELECT status FROM sync_queue WHERE id = $1")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "gl"
|
||||
description = "The Twigpine CLI — identity management, node control, MCP server"
|
||||
version = "0.8.0" # x-release-please-version
|
||||
version = "0.7.1" # x-release-please-version
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "icaptcha-client"
|
||||
version = "0.8.0" # x-release-please-version
|
||||
version = "0.4.0" # x-release-please-version
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
Reference in new issue
Block a user