Skip to main content

alkgit_substrate/
lfs_adapter.rs

1//! The LFS batch/transfer adapter (wave 9) — the storage-side logic of
2//! the LFS protocol's batch/checking and byte paths, as a substrate
3//! surface under the `lfs` feature (ADR-022 §1–2; lfs.md §1–2; the
4//! substrate-side piece of ADR-025's LFS-surface member). The alkgit
5//! core has no native LFS op set (git-lfs speaks HTTP only) — the door
6//! is the alkhttp `lfs` feature (family infrastructure); this surface
7//! is what that door consumes, front-door-blind: it carries the
8//! storage-side mapping and never touches HTTP.
9//!
10//! # The adapter/door split (the wave's spec-level call, pinned here
11//! for the gate)
12//!
13//! ADR-025's neutral record names the door-side batch mapping as
14//! riding alkblobs directly; the row assertion is the
15//! composition-owned piece only this surface carries. Drawn exactly:
16//!
17//! **The door's side (never here):** the JSON batch protocol parsing
18//! and the HTTP route surface; the auth plumbing (alkhttp ADR-073's
19//! Basic-userinfo tolerance on the batch); the **anonymous-public
20//! `anon` identity synthesis** (the adapter refuses an absent
21//! identity — the door synthesizes the identity, the grant
22//! short-circuits on the public-repo flag); **href/token minting**
23//! ([`LfsHrefMint`] is the mint-input carrier — the door mints the
24//! absolute URLs with per-action `Authorization`; the adapter never
25//! mints a URL or a door token); HTTP `Range`-header parsing and the
26//! 206/416 responses; the object-size ceiling choice (the budget per
27//! assembly, the door's [`LfsAdapterConfig::max_object_bytes`]); and
28//! the batch request's `transfers` negotiation (the adapter carries
29//! no negotiation input and answers `basic` — the door reads the
30//! client's list and refuses a request that excludes `basic` before
31//! the batch reaches the adapter; the wave-9 gate's recorded door
32//! duty, lfs.md §1's pinned shape).
33//!
34//! **The adapter's side (this surface):** the batch's per-object
35//! existence answered over the ops control plane — `blobs/stat`
36//! through an
37//! [`OperationContext`]
38//! built with the caller's identity and the *wired* ownership
39//! provider (the POC-L door's DirectOps binding shape as a substrate
40//! surface); the response objects' shapes (`{oid, size,
41//! authenticated}` + the per-object action grant-carriers); **the row
42//! assertion at check time** (the moment the server learns the pair —
43//! lfs.md §3, over the [`LfsRows`] surface); the
44//! byte paths: the **pre-keyed put** (upload) and the **ranged fetch
45//! with per-window slice-digest verification** (download); the
46//! wired-provider requirement (never the static fallback).
47//!
48//! **What rides alkblobs directly (no substrate surface):** the JSON
49//! batch protocol itself is the door's; the facade seam
50//! (`Store::put_as(GitObjectSha256, len, stream)` / `read_range`) is
51//! the in-process embedder alternative with no ops plane at all —
52//! deliberately not re-exposed here (the embedder binds
53//! [`SubstrateAssembly::blobs`](crate::compose::SubstrateAssembly::blobs)
54//! directly; the adapter is the ops-plane surface). The wire ops
55//! (`blobs/put` / `blobs/fetch`, the channel data plane) remain the
56//! fleet/remote consumer's form — the DirectOps-vs-loopback binding
57//! choice sits door-side.
58//!
59//! # The pinned protocol facts (lfs.md §1, riding the shapes)
60//!
61//! - `hash_algo` answers `sha256` only: a batch carrying any other
62//!   declared algorithm is refused ([`LfsAdapterError::HashAlgo`] —
63//!   the door's 409). `verify` is not served: no batch path produces
64//!   a verify action.
65//! - Transfer is `basic`; the download action's mint input is the
66//!   whole media oid (range negotiation never rides the batch — the
67//!   client's `Range` rides the transfer GET).
68//! - The upload key is **formable in advance** (ADR-022 §1: under
69//!   `Algorithm::GitObjectSha256` the oid *is* the digest — the key is
70//!   `[0x04] + oid bytes`): existence is a direct `blobs/stat` on the
71//!   formed key (the translate-then-stat is direct; there is no
72//!   mapping row, and no `have` use — that is the replication diff
73//!   verb).
74//!
75//! # The byte paths (ADR-022 §2)
76//!
77//! - **Upload — the pre-keyed put.** The offer carries the claimed
78//!   oid and the batch's size; a size-less offer is a protocol error
79//!   ([`LfsAdapterError::SizelessOffer`] — LFS clients always send
80//!   size; the door refuses before the body). The put rides the put
81//!   handler's contract minus the channel: the one-read tee (a sha256
82//!   pass alongside a bounded pipe the facade put consumes
83//!   concurrently via `put_as(GitObjectSha256, Some(len), …)`), the
84//!   claim cross-check **before the facade's EOF** — a body the
85//!   derivation contradicts kills the put pre-commit
86//!   ([`LfsAdapterError::MislabeledBody`]: no stored content, no held
87//!   pin — the zero-residue discipline the put's stage hygiene
88//!   carries on both tiers) — then the custody hand-over through the
89//!   [`OpsContext`] verbs: fleet
90//!   (`mint_fleet`, then the immediate `confirm` — the upload is
91//!   complete, so the token's ticket retires at the confirmation and
92//!   the mint boundary's admission cap stays live per upload, ADR-022
93//!   §5; the fleet pin row's TTL is the durable protection — a
94//!   dropped unconfirmed ticket would hold an admission slot until
95//!   process end, the wave-9 gate's fix) or non-fleet
96//!   (`mint_custodial` + `confirm`, the LFS pattern POC-L validated;
97//!   a mint refusal at
98//!   the admission cap is [`LfsAdapterError::MintRefused`], answering
99//!   the already-committed put honestly). The reference row is
100//!   asserted at upload time too (batch check or upload — lfs.md §3).
101//! - **Download — the ranged fetch.** Every download serves verified
102//!   windows: the requested range (or the whole entry) is split
103//!   internally at [`LfsAdapterConfig::window_bytes`] and each window
104//!   rides `Store::read_range`, whose ADR-006 slice digest is checked
105//!   mid-flight — a window's bytes are yielded only after the
106//!   independently computed sha256 of exactly the returned window
107//!   bytes matches the read's slice digest; a mismatch aborts the
108//!   stream mid-flight (the caller re-fetches — the ranged arm's
109//!   answer is the mid-flight-verified shape). **The resolved span
110//!   is budget-gated** by [`LfsAdapterConfig::max_object_bytes`] —
111//!   every form the adapter will actually produce counts (the
112//!   range-less whole serve, the open-ended resume, any resolved
113//!   span; `bytes=0-` is a whole download and refuses over the
114//!   ceiling — the wave-9 gate's fix, the whole-arm budget's
115//!   bypass closed). **Verification, stated precisely** (the
116//!   gate's fix): a whole-span download additionally rides the
117//!   end-to-end oid digest check — the windows' bytes accumulate
118//!   into one sha256 pass, and the finished digest is compared
119//!   against the key's derivation digest (the identity family's
120//!   digest is the raw content's hash, so the comparison is the oid
121//!   verification the large tier's reads carry no re-derivation
122//!   for) **before the last window yields**. A partial span's
123//!   windows carry the length + slice checks only — the store
124//!   interior's ADR-006 digests, not an oid conclusion. **The
125//!   pinned window-splitting call** (drawn against lfs.md's "the
126//!   door owns window splitting", flagged for the gate): that fact
127//!   governs the door binding the *ops byte-path* (the
128//!   loopback/remote forms, where `fetch_max_total_bytes` bounds
129//!   the op's ranged arm). This side of the HTTP boundary the
130//!   adapter owns the arithmetic: a download call splits internally
131//!   at `window_bytes` per verified window; the door's HTTP work is
132//!   Range parsing and the budget, not window counting.
133//!
134//! # The wired ownership provider (alkcall ADR-048's posture, composed)
135//!
136//! The adapter takes the ownership provider **as a required
137//! constructor argument** — there is no Option, no static path, and
138//! every context it builds wires
139//! `ownership: Some(provider)`: the static `Identity.resources`
140//! fallback is unreachable through this surface (the POC-L
141//! gateway-gap regression's posture; the battery pins it — an
142//! identity carrying a static resources claim is refused unless the
143//! wired provider grants). The shipped provider is
144//! [`LfsGrantProvider`]: alkgit's registry composed onto the ops ACL
145//! — the same per-repo grants the git surface composes (ADR-011's
146//! posture), served through the cached read provider's sync resolve.
147//! alkcall ADR-048's loud default guards the gateway dispatch spine
148//! (the wire path); here the guard is the constructor requirement
149//! plus the battery. The `anon` composition is door-side (above).
150
151use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
152use std::sync::Arc;
153use std::task::{Context as PollContext, Poll};
154use std::time::{SystemTime, UNIX_EPOCH};
155
156use alkblobs::ops::{stat_pair, OpsConfig, OpsContext, PinConverter, BLOB_MISSING};
157use alkblobs::{Key, Pin, Store as BlobStore};
158use alkcall::core::auth::Identity;
159use alkcall::core::ownership::OwnershipProvider;
160use alkcall::core::types::Capabilities;
161use alkcall::registry::context::{AbortPolicy, OperationContext, ScopedPeerEnv};
162use alkcall::registry::env::{LocalOperationEnv, OperationEnv};
163use alkcall::registry::registration::OperationRegistry;
164use alkcall::OperationRegistryBuilder;
165use sha2::{Digest as _, Sha256};
166use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf};
167use tokio::sync::mpsc;
168
169use crate::error::OpenError;
170use crate::lfs::LfsRows;
171use crate::liveness::lfs_media_key;
172use crate::registry::RegistryRead;
173
174/// The batch/transfer adapter's knobs (the door budgets the ceiling;
175/// the adapter splits at the window). Defaults are the POC-L datums:
176/// the 32 MiB split window and the `fetch_max_total_bytes`-shaped
177/// 64 MiB object ceiling (raised per assembly to trade opens for
178/// windows — lfs.md §2).
179#[derive(Clone, Debug, PartialEq, Eq)]
180pub struct LfsAdapterConfig {
181    /// The download windows' boundary — the size each verified
182    /// `read_range` window serves before the next (the POC-L split
183    /// datum, 32 MiB). Must be non-zero.
184    pub window_bytes: u64,
185    /// The download span ceiling (the door's budget per assembly —
186    /// lfs.md §2's "the door budgets the whole arm", realized
187    /// adapter-side): the **resolved span** of any download form is
188    /// the budgeted quantity — a range-less whole serve, an
189    /// open-ended resume (`bytes=0-`), or any resolved span over the
190    /// ceiling refuses [`LfsAdapterError::TooLarge`]; a partial span
191    /// under it rides the window arithmetic (a resume's legitimate
192    /// use).
193    pub max_object_bytes: u64,
194}
195
196impl Default for LfsAdapterConfig {
197    fn default() -> Self {
198        LfsAdapterConfig {
199            window_bytes: 32 * 1024 * 1024,
200            max_object_bytes: 64 * 1024 * 1024,
201        }
202    }
203}
204
205/// The batch API's operation half (the door maps the wire's
206/// `"upload"` / `"download"` onto it).
207#[derive(Clone, Copy, Debug, PartialEq, Eq)]
208pub enum LfsBatchOperation {
209    /// The client means to push media (the absent response objects
210    /// carry upload-action mint inputs).
211    Upload,
212    /// The client means to pull media (the present response objects
213    /// carry download-action mint inputs).
214    Download,
215}
216
217impl LfsBatchOperation {
218    /// The wire form's parse (the door's mapping input); anything
219    /// else is not a batch operation.
220    pub fn from_wire(wire: &str) -> Option<Self> {
221        match wire {
222            "upload" => Some(LfsBatchOperation::Upload),
223            "download" => Some(LfsBatchOperation::Download),
224            _ => None,
225        }
226    }
227}
228
229/// One batch request object: the pointer's oid and its mandatory
230/// size (the batch's size is mandatory — LFS clients always send it;
231/// the door refuses the size-less form before this surface).
232#[derive(Clone, Debug, PartialEq, Eq)]
233pub struct LfsBatchObject {
234    /// The LFS media oid (the raw sha256 of the content — 64 hex, the
235    /// media family's digest; case as the client sent it, the
236    /// validated form is lowercased).
237    pub oid: String,
238    /// The mandatory asserted media size.
239    pub size: u64,
240}
241
242/// The batch/checking request as the door hands it over (post-JSON,
243/// post-auth plumbing — the identity resolved, the operation mapped).
244#[derive(Clone, Debug, PartialEq, Eq)]
245pub struct LfsBatchRequest {
246    /// The batch's operation half.
247    pub operation: LfsBatchOperation,
248    /// The client's declared hash algorithm (`None` = the default).
249    /// Anything but `"sha256"` is refused (the pinned protocol fact).
250    pub hash_algo: Option<String>,
251    /// The requested objects.
252    pub objects: Vec<LfsBatchObject>,
253}
254
255/// The action-grant carrier the response objects hold — the **mint
256/// inputs**, never a URL: the door mints the absolute href and the
257/// per-action `Authorization` from these (front-door-blind — no URL
258/// or door token is formed here).
259#[derive(Clone, Debug, PartialEq, Eq)]
260pub struct LfsHrefMint {
261    /// The repo id (the ops namespace — the href's repo-path input).
262    pub repo_id: String,
263    /// The object's oid (the href's object-path input).
264    pub oid: String,
265}
266
267/// The per-object action grants (the LFS shape: `upload` rides the
268/// absent objects' answers for an upload batch, `download` the
269/// present ones for a download batch; at most one side is populated
270/// per response object).
271#[derive(Clone, Debug, PartialEq, Eq)]
272pub struct LfsActions {
273    /// The upload-action mint input (present when the media is
274    /// absent and the caller means to upload).
275    pub upload: Option<LfsHrefMint>,
276    /// The download-action mint input (present when the media is
277    /// present and the caller means to download).
278    pub download: Option<LfsHrefMint>,
279}
280
281/// A per-object error, carried in the response object (the LFS
282/// per-object error shape — the object's `error` field; the HTTP
283/// mapping stays the door's).
284#[derive(Clone, Debug, PartialEq, Eq)]
285pub struct LfsObjectError {
286    /// The LFS error code (403 refused, 404 not found, 422
287    /// malformed / disagreement, 500 server).
288    pub code: u16,
289    /// The stable message text.
290    pub message: &'static str,
291}
292
293impl LfsObjectError {
294    /// The refused-per-grant object.
295    fn refused() -> Self {
296        LfsObjectError {
297            code: 403,
298            message: "refused by grant",
299        }
300    }
301
302    /// The absent-object error (a download batch's miss).
303    fn not_found() -> Self {
304        LfsObjectError {
305            code: 404,
306            message: "object not found",
307        }
308    }
309
310    /// The malformed-oid error (not a 64-lowercase-hex sha256 form).
311    fn malformed() -> Self {
312        LfsObjectError {
313            code: 422,
314            message: "malformed oid",
315        }
316    }
317
318    /// The size-disagreement error (the store entry's length and the
319    /// asserted size disagree — the pointer contradicts the content).
320    fn disagree() -> Self {
321        LfsObjectError {
322            code: 422,
323            message: "size disagreement",
324        }
325    }
326
327    /// The storage-side failure error.
328    fn server() -> Self {
329        LfsObjectError {
330            code: 500,
331            message: "server error",
332        }
333    }
334}
335
336/// One batch response object — the pinned shape `{oid, size,
337/// authenticated}` plus the optional per-object error/actions
338/// branches. `authenticated` is `true` whenever the object is
339/// answered: the request rode the wired ownership provider's grant
340/// (the ops ACL path), so every served action is server-authorized —
341/// the anonymous-public composition (the door's synthesized `anon`
342/// identity on a public repo's read) answers the same way.
343#[derive(Clone, Debug, PartialEq, Eq)]
344pub struct LfsObjectAnswer {
345    /// The object's oid, echoed in the validated lowercase form.
346    pub oid: String,
347    /// The object's asserted size.
348    pub size: u64,
349    /// The server-authorized flag (see the type doc).
350    pub authenticated: bool,
351    /// The per-object error branch (`None` for answered objects).
352    pub error: Option<LfsObjectError>,
353    /// The per-object action branch (`None` when there is nothing to
354    /// do — the already-present object of an upload batch, the
355    /// idempotent re-push's posture).
356    pub actions: Option<LfsActions>,
357}
358
359/// The batch/checking response — the transfer is `basic` and the
360/// served algorithm is `sha256` (the pinned protocol facts; the door
361/// serializes the JSON shape).
362#[derive(Clone, Debug, PartialEq, Eq)]
363pub struct LfsBatchResponse {
364    /// The negotiated transfer adapter (`"basic"`).
365    pub transfer: &'static str,
366    /// The served hash algorithm (`"sha256"` — the only one).
367    pub hash_algo: &'static str,
368    /// One answer per request object, request order.
369    pub objects: Vec<LfsObjectAnswer>,
370}
371
372/// The byte range a download call serves: `[offset, offset + len)`
373/// with `len = None` meaning to the entry's end (the door maps the
374/// HTTP `bytes=a-b` / `bytes=a-` forms onto the explicit count).
375#[derive(Clone, Copy, Debug, PartialEq, Eq)]
376pub struct LfsRange {
377    /// The range's first byte offset.
378    pub offset: u64,
379    /// The range's byte count (`None` = through the entry's end).
380    pub len: Option<u64>,
381}
382
383/// The adapter's error surface (thiserror family; message-only —
384/// the engine source types stay off the surface, the row store's
385/// [`StorageError`](alkgit::StorageError) text is carried). The
386/// whole-batch failures (forbidden, hash-algo) and the transfer-path
387/// failures are errors; per-object failures ride the batch response's
388/// error objects, not this surface.
389#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
390pub enum LfsAdapterError {
391    /// The caller is unauthorized (no identity, or the wired provider
392    /// does not grant the operation's action on the namespace). The
393    /// door's mapping is the 401 / generic denial (unknown ≡
394    /// unauthorized).
395    #[error("forbidden: {0}")]
396    Forbidden(String),
397    /// The batch declared a hash algorithm other than `sha256` (the
398    /// pinned 409 fact).
399    #[error("unsupported hash_algo {0}: the server serves sha256 only")]
400    HashAlgo(String),
401    /// The transfer payload is malformed (a non-oid form).
402    #[error("invalid LFS input: {0}")]
403    Invalid(String),
404    /// The whole download's object exceeds the configured ceiling.
405    #[error("object of {0} bytes exceeds the configured download ceiling")]
406    TooLarge(u64),
407    /// The download's byte range names nothing the entry serves
408    /// (a past-EOF offset — the door's 416).
409    #[error("the requested range is not satisfiable")]
410    NotSatisfiable,
411    /// The storage read or the serving machinery failed.
412    #[error("storage: {0}")]
413    Storage(String),
414    /// The row assertion failed.
415    #[error("lfs rows: {0}")]
416    Rows(String),
417    /// The upload's offer carried no size (the pre-keyed put's
418    /// mandatory offer — the pinned protocol refusal).
419    #[error("size-less offer refused: the pre-keyed put requires the batch's size")]
420    SizelessOffer,
421    /// The upload's body failed its claim: the stream's sha256 is
422    /// not the claimed oid (the put's contract — nothing stored, no
423    /// held pin; killed pre-EOF, zero residue).
424    #[error("mislabeled body: the stream's digest contradicts the claimed oid {0}")]
425    MislabeledBody(String),
426    /// The custody hand-over failed post-commit (the confirm verb
427    /// refused, the mint failed operationally — the put landed; the
428    /// custody question is the operator's).
429    #[error("custody: {0}")]
430    Custody(String),
431    /// The token mint was refused at the registry's admission cap
432    /// (alkblobs ADR-019): the put already committed and verified;
433    /// no token issued, no custody taken — the re-put is
434    /// CAS-idempotent.
435    #[error("mint refused at the token registry's admission cap: {0}")]
436    MintRefused(String),
437}
438
439/// The wired ownership provider — alkgit's registry composed onto the
440/// ops ACL (the batch's authorization posture: lfs.md §1 — the
441/// per-repo grant governs batch access, the ADR-011 posture composed
442/// onto the ops ACL). The alkblobs ops specs gate on `blob-namespace`
443/// resources whose id is the payload's namespace — the repo id; this
444/// provider answers with the same [`authorize`](alkgit::authorize)
445/// policy function the git surface composes: `read` granted (or
446/// anonymous-public — the door's synthesized `anon` identity rides
447/// the public-repo flag here), `write` per the grant monotonicity
448/// (`manage` ⊃ `write` ⊃ `read`). Unknown repos deny by default —
449/// the unknown ≡ unauthorized posture. Served through the cached
450/// read provider's sync resolve (the hot path — no await, no round
451/// trip; a stale cache delays revocation until the next refresh, the
452/// same posture the git serving path takes).
453pub struct LfsGrantProvider {
454    provider: Arc<crate::registry::CachedRegistryProvider>,
455}
456
457impl LfsGrantProvider {
458    /// The provider over a cached read provider (the assembly's —
459    /// [`SubstrateAssembly::provider`](crate::compose::SubstrateAssembly::provider)).
460    pub fn new(provider: Arc<crate::registry::CachedRegistryProvider>) -> Self {
461        LfsGrantProvider { provider }
462    }
463}
464
465impl OwnershipProvider for LfsGrantProvider {
466    fn owns(
467        &self,
468        identity: &Identity,
469        resource_type: &str,
470        resource_id: &str,
471        action: &str,
472    ) -> bool {
473        if resource_type != "blob-namespace" {
474            return false;
475        }
476        let action = match action {
477            "read" => alkgit::AccessAction::Read,
478            "write" => alkgit::AccessAction::Write,
479            _ => return false,
480        };
481        match self.provider.resolve(resource_id) {
482            Some(record) => alkgit::authorize(&record, Some(identity), action),
483            None => false,
484        }
485    }
486
487    fn owned_resources(&self, identity: &Identity, resource_type: &str) -> Vec<String> {
488        if resource_type != "blob-namespace" {
489            return Vec::new();
490        }
491        let snapshot = self.provider.snapshot();
492        let mut owned = Vec::new();
493        for (repo_id, record) in snapshot.iter() {
494            if alkgit::authorize(record, Some(identity), alkgit::AccessAction::Read) {
495                owned.push(repo_id.clone());
496            }
497        }
498        owned.sort();
499        owned
500    }
501
502    fn owns_any(&self, identity: &Identity, resource_type: &str) -> bool {
503        !self.owned_resources(identity, resource_type).is_empty()
504    }
505}
506
507/// The LFS put's converter — the substrate's `PinConverter`
508/// registration act (ADR-022 §4's pinned sentence, realized): the
509/// converter stays what alkblobs pins it to be (sync-only,
510/// failure-free, `Key`-only) and registers **nothing** — the rich
511/// reference metadata lives in alkgit's rows (asserted by this
512/// surface before the confirm), and the entry's store-side
513/// protection is the LFS media liveness source registered at the
514/// composition entry (the fail-safe default), which consults the
515/// rows. The confirm verb demands *a* converter per namespace (an
516/// unbound converter refuses, mapping intact — alkblobs ADR-010 §4);
517/// this is the honest act.
518struct RowsAssertedConverter;
519
520impl PinConverter for RowsAssertedConverter {
521    fn convert(&self, _key: &Key) {}
522}
523
524/// The batch/transfer adapter — the storage-side surface the alkhttp
525/// `lfs` door consumes (ADR-022 §1–3, lfs.md §1–2 — the
526/// adapter/door split is the module doc). Construct over the object
527/// pool, the rows surface, the **wired** ownership provider
528/// (required — see the module doc), and the config knobs.
529pub struct LfsAdapter {
530    ops: Arc<OpsContext>,
531    registry: Arc<OperationRegistry>,
532    env: Arc<LocalOperationEnv>,
533    rows: Arc<dyn LfsRows>,
534    ownership: Arc<dyn OwnershipProvider>,
535    config: LfsAdapterConfig,
536    requests: AtomicU64,
537}
538
539impl LfsAdapter {
540    /// Construct the adapter. The registry assembled here is the ops
541    /// control plane the batch answers over (the POC-L DirectOps
542    /// binding shape): `blobs/stat` through the [`OpsContext`], the
543    /// namespace ACL riding the wired provider — the one op this
544    /// surface registers, so the ops fetch op's own knobs
545    /// (`fetch_max_total_bytes` and friends) stay at their defaults
546    /// and are not this surface's budget: downloads ride
547    /// `read_range` directly under the resolved-span ceiling
548    /// ([`LfsAdapterConfig::max_object_bytes`], enforced adapter-side).
549    /// Fallible on the config's shape (the fallible-constructor
550    /// posture).
551    pub fn new(
552        store: Arc<BlobStore>,
553        rows: Arc<dyn LfsRows>,
554        ownership: Arc<dyn OwnershipProvider>,
555        config: LfsAdapterConfig,
556    ) -> Result<Self, OpenError> {
557        if config.window_bytes == 0 {
558            return Err(OpenError::Invalid(
559                "the LFS adapter's window_bytes must be non-zero".to_owned(),
560            ));
561        }
562        let ops = Arc::new(OpsContext::new(Arc::clone(&store), OpsConfig::default()));
563        let (spec, handler) = stat_pair(Arc::clone(&ops));
564        let registry = Arc::new(
565            OperationRegistryBuilder::new()
566                .with_local(spec, handler, None, None, Capabilities::new())
567                .map_err(|e| OpenError::Invalid(format!("the ops registry assembly: {e}")))?
568                .build(),
569        );
570        let env = Arc::new(LocalOperationEnv::new(Arc::clone(&registry)));
571        Ok(LfsAdapter {
572            ops,
573            registry,
574            env,
575            rows,
576            ownership,
577            config,
578            requests: AtomicU64::new(0),
579        })
580    }
581
582    /// The adapter's config (the door reads the budget knobs).
583    pub fn config(&self) -> &LfsAdapterConfig {
584        &self.config
585    }
586
587    /// The per-request `OperationContext` — the POC-L door's DirectOps
588    /// posture: the caller's identity, the wired ownership provider
589    /// (always `Some` — never the static fallback), the local
590    /// operation env, abort defaults. A request id forms from a
591    /// per-adapter counter (correlation data; the wire never sees it).
592    fn context(&self, identity: Option<Identity>) -> OperationContext {
593        let nanos = SystemTime::now()
594            .duration_since(UNIX_EPOCH)
595            .map(|d| d.as_nanos())
596            .unwrap_or_default();
597        OperationContext {
598            request_id: format!(
599                "lfs-adapter-{}-{nanos}",
600                self.requests.fetch_add(1, Ordering::Relaxed)
601            ),
602            parent_request_id: None,
603            identity,
604            handler_identity: None,
605            forwarded_for: None,
606            capabilities: Capabilities::new(),
607            metadata: std::collections::HashMap::new(),
608            scoped_env: ScopedPeerEnv::empty(),
609            env: Arc::clone(&self.env) as Arc<dyn OperationEnv>,
610            abort_policy: AbortPolicy::default(),
611            deadline: None,
612            internal: false,
613            ownership: Some(Arc::clone(&self.ownership)),
614        }
615    }
616
617    /// The batch/checking answer (lfs.md §1): the formable-key
618    /// existence over the ops control plane (`blobs/stat` through the
619    /// context), the response objects' shapes, and the row assertion
620    /// at check time — every validated object's `(repo_id, oid, size)`
621    /// pair is asserted **before** the probes (the moment the server
622    /// learns the pair; a malformed oid cannot form a row and its
623    /// answer is the per-object 422). The batch's own gates run
624    /// first: no identity, or the wired provider not granting the
625    /// operation's action (`write` for upload batches, `read` for
626    /// download batches), is the forbidden refusal; a declared
627    /// `hash_algo` other than `sha256` is the pinned 409.
628    pub async fn check(
629        &self,
630        identity: Option<&Identity>,
631        repo_id: &str,
632        request: LfsBatchRequest,
633    ) -> Result<LfsBatchResponse, LfsAdapterError> {
634        let identity = require_identity(identity)?;
635        if let Some(algo) = &request.hash_algo {
636            if algo != "sha256" {
637                return Err(LfsAdapterError::HashAlgo(algo.clone()));
638            }
639        }
640        let action = match request.operation {
641            LfsBatchOperation::Upload => "write",
642            LfsBatchOperation::Download => "read",
643        };
644        if !self
645            .ownership
646            .owns(&identity, "blob-namespace", repo_id, action)
647        {
648            return Err(LfsAdapterError::Forbidden(format!(
649                "not granted: blob-namespace/{repo_id} {action}"
650            )));
651        }
652
653        for object in &request.objects {
654            if lfs_media_key(&object.oid).is_ok() {
655                self.rows
656                    .assert(repo_id, &lower_hex(&object.oid), object.size)
657                    .await
658                    .map_err(|e| LfsAdapterError::Rows(e.to_string()))?;
659            }
660        }
661
662        let mut objects = Vec::with_capacity(request.objects.len());
663        for object in &request.objects {
664            let oid = lower_hex(&object.oid);
665            let answer = match lfs_media_key(&oid) {
666                Err(_) => LfsObjectAnswer {
667                    oid,
668                    size: object.size,
669                    authenticated: true,
670                    error: Some(LfsObjectError::malformed()),
671                    actions: None,
672                },
673                Ok(key) => {
674                    self.answers(
675                        &identity,
676                        repo_id,
677                        request.operation,
678                        &oid,
679                        object.size,
680                        &key,
681                    )
682                    .await
683                }
684            };
685            objects.push(answer);
686        }
687        Ok(LfsBatchResponse {
688            transfer: "basic",
689            hash_algo: "sha256",
690            objects,
691        })
692    }
693
694    /// One validated request object's answer: the stat probe over the
695    /// control plane, then the operation's branch (the mint inputs —
696    /// never URLs — for the to-be-uploaded absent object or the
697    /// downloadable present one, the per-object error shapes).
698    async fn answers(
699        &self,
700        identity: &Identity,
701        repo_id: &str,
702        operation: LfsBatchOperation,
703        oid: &str,
704        size: u64,
705        key: &Key,
706    ) -> LfsObjectAnswer {
707        match self.stat(identity, repo_id, key).await {
708            StatProbe::Missing => match operation {
709                LfsBatchOperation::Upload => LfsObjectAnswer {
710                    oid: oid.to_owned(),
711                    size,
712                    authenticated: true,
713                    error: None,
714                    actions: Some(LfsActions {
715                        upload: Some(LfsHrefMint {
716                            repo_id: repo_id.to_owned(),
717                            oid: oid.to_owned(),
718                        }),
719                        download: None,
720                    }),
721                },
722                LfsBatchOperation::Download => LfsObjectAnswer {
723                    oid: oid.to_owned(),
724                    size,
725                    authenticated: true,
726                    error: Some(LfsObjectError::not_found()),
727                    actions: None,
728                },
729            },
730            StatProbe::Length(len) if len == size => match operation {
731                LfsBatchOperation::Upload => LfsObjectAnswer {
732                    oid: oid.to_owned(),
733                    size,
734                    authenticated: true,
735                    error: None,
736                    actions: None,
737                },
738                LfsBatchOperation::Download => LfsObjectAnswer {
739                    oid: oid.to_owned(),
740                    size,
741                    authenticated: true,
742                    error: None,
743                    actions: Some(LfsActions {
744                        upload: None,
745                        download: Some(LfsHrefMint {
746                            repo_id: repo_id.to_owned(),
747                            oid: oid.to_owned(),
748                        }),
749                    }),
750                },
751            },
752            StatProbe::Length(_) => LfsObjectAnswer {
753                oid: oid.to_owned(),
754                size,
755                authenticated: true,
756                error: Some(LfsObjectError::disagree()),
757                actions: None,
758            },
759            StatProbe::Forbidden => LfsObjectAnswer {
760                oid: oid.to_owned(),
761                size,
762                authenticated: false,
763                error: Some(LfsObjectError::refused()),
764                actions: None,
765            },
766            StatProbe::Failed => LfsObjectAnswer {
767                oid: oid.to_owned(),
768                size,
769                authenticated: true,
770                error: Some(LfsObjectError::server()),
771                actions: None,
772            },
773        }
774    }
775
776    /// The ops control plane's stat probe (the formable key's
777    /// existence): `blobs/stat` invoked directly on the assembled
778    /// registry through a per-object context — the ACL runs exactly
779    /// as on the wire (the `resource_id_path` selects the namespace).
780    async fn stat(&self, identity: &Identity, namespace: &str, key: &Key) -> StatProbe {
781        let envelope = self
782            .registry
783            .invoke(
784                "blobs/stat",
785                serde_json::json!({
786                    "namespace": namespace,
787                    "digest": key_hex(key),
788                }),
789                self.context(Some(identity.clone())),
790            )
791            .await;
792        match envelope.result {
793            Ok(payload) => {
794                let len = payload
795                    .get("len")
796                    .and_then(serde_json::Value::as_u64)
797                    .unwrap_or_default();
798                StatProbe::Length(len)
799            }
800            Err(error) if error.code == BLOB_MISSING => StatProbe::Missing,
801            Err(error) if error.code == "FORBIDDEN" => StatProbe::Forbidden,
802            Err(_) => StatProbe::Failed,
803        }
804    }
805
806    /// The upload — the pre-keyed put (ADR-022 §2). The identity and
807    /// the write grant are required (the wired provider; the door
808    /// carries the Basic tolerance). The size is the put's mandatory
809    /// claim: `None` is the size-less refusal. The row is asserted
810    /// first (the moment the upload learns the pair), then the put
811    /// rides the put handler's contract minus the channel: the
812    /// one-read tee over a bounded pipe, the claim cross-check
813    /// pre-EOF (a mislabeled body kills the put — zero residue), then
814    /// the custody hand-over through the `OpsContext` verbs — mint
815    /// and confirm (the fleet form mints and drops the guard; the
816    /// non-fleet form mints custodial and confirms, the POC-L
817    /// attach-reference pattern).
818    pub async fn upload<R>(
819        &self,
820        identity: Option<&Identity>,
821        repo_id: &str,
822        oid: &str,
823        size: Option<u64>,
824        body: R,
825    ) -> Result<(), LfsAdapterError>
826    where
827        R: AsyncRead + Unpin + Send,
828    {
829        let identity = require_identity(identity)?;
830        if !self
831            .ownership
832            .owns(&identity, "blob-namespace", repo_id, "write")
833        {
834            return Err(LfsAdapterError::Forbidden(format!(
835                "not granted: blob-namespace/{repo_id} write"
836            )));
837        }
838        let size = size.ok_or(LfsAdapterError::SizelessOffer)?;
839        let oid = lower_hex(oid);
840        let expected = lfs_media_key(&oid).map_err(|e| LfsAdapterError::Invalid(e.to_string()))?;
841
842        self.rows
843            .assert(repo_id, &oid, size)
844            .await
845            .map_err(|e| LfsAdapterError::Rows(e.to_string()))?;
846
847        let (key, pin) =
848            pre_keyed_put(Arc::clone(self.ops.store()), &expected, &oid, size, body).await?;
849
850        self.hand_over(repo_id, key, pin).await
851    }
852
853    /// The custody hand-over post-commit (the put handler's finish
854    /// arm plus the LFS confirm): fleet — `mint_fleet` over the
855    /// committed row (the owner is the serving instance's id, minted
856    /// against the store's fleet declaration) then an immediate
857    /// `confirm` through [`RowsAssertedConverter`]: the upload is
858    /// complete before the hand-over, so the token's ticket retires
859    /// at the confirmation (the mint boundary's admission cap stays
860    /// live per upload — ADR-022 §5's hostile-loop posture — while a
861    /// dropped unconfirmed ticket would hold an admission slot until
862    /// process end, the wave-9 gate's fix); the fleet pin row's TTL
863    /// remains the durable protection. Non-fleet — `mint_custodial`
864    /// (the registry takes the guard) then `confirm`. A mint refused
865    /// at the cap answers the already-committed put honestly
866    /// ([`LfsAdapterError::MintRefused`]; the re-put is
867    /// CAS-idempotent); an operational mint failure or a refused
868    /// confirm is [`LfsAdapterError::Custody`].
869    async fn hand_over(&self, namespace: &str, key: Key, pin: Pin) -> Result<(), LfsAdapterError> {
870        let refused = |key: &Key| {
871            LfsAdapterError::MintRefused(format!(
872                "no token issued, no custody taken; the committed key {} stands — the re-put is \
873                 CAS-idempotent",
874                hex_of(key.digest().bytes()),
875            ))
876        };
877        match self.ops.fleet_owner() {
878            Some(fleet) => match self.ops.mint_fleet(namespace, key.clone(), &fleet) {
879                Ok(token) => {
880                    self.ops
881                        .bind_converter(namespace, Arc::new(RowsAssertedConverter));
882                    if self.ops.confirm(namespace, &token) {
883                        drop(pin);
884                        Ok(())
885                    } else {
886                        drop(pin);
887                        Err(LfsAdapterError::Custody(
888                            "the confirm conversion refused (the mapping stands intact)".to_owned(),
889                        ))
890                    }
891                }
892                Err(alkblobs::Error::RegistryFull) => Err(refused(&key)),
893                Err(e) => {
894                    drop(pin);
895                    Err(LfsAdapterError::Custody(e.to_string()))
896                }
897            },
898            None => {
899                self.ops
900                    .bind_converter(namespace, Arc::new(RowsAssertedConverter));
901                match self.ops.mint_custodial(namespace, key.clone(), pin) {
902                    Ok(token) if self.ops.confirm(namespace, &token) => Ok(()),
903                    Ok(_token) => Err(LfsAdapterError::Custody(
904                        "the confirm conversion refused (the mapping stands intact)".to_owned(),
905                    )),
906                    Err(alkblobs::Error::RegistryFull) => Err(refused(&key)),
907                    Err(e) => Err(LfsAdapterError::Custody(e.to_string())),
908                }
909            }
910        }
911    }
912
913    /// The download — the ranged fetch (ADR-022 §2): the read grant is
914    /// required (the wired provider), the entry's existence rides
915    /// `stat`, and the served stream verifies per-window slice
916    /// digests mid-flight (the module doc's byte-path section). The
917    /// resolved span is budget-gated by
918    /// [`LfsAdapterConfig::max_object_bytes`] — every form (the
919    /// range-less whole serve, the open-ended resume, any resolved
920    /// span) the adapter will actually produce counts against the
921    /// ceiling; a past-EOF offset is the not-satisfiable refusal (the
922    /// door's 416). A whole-span download additionally verifies the
923    /// served bytes' sha256 against the oid end-to-end at EOF (the
924    /// large tier's reads carry no canonical re-derivation — the
925    /// oid check is this path's job); a partial span's windows carry
926    /// the length + store-interior slice checks only (the module
927    /// doc's verification statement).
928    pub async fn download(
929        &self,
930        identity: Option<&Identity>,
931        repo_id: &str,
932        oid: &str,
933        range: Option<LfsRange>,
934    ) -> Result<LfsDownload, LfsAdapterError> {
935        let identity = require_identity(identity)?;
936        if !self
937            .ownership
938            .owns(&identity, "blob-namespace", repo_id, "read")
939        {
940            return Err(LfsAdapterError::Forbidden(format!(
941                "not granted: blob-namespace/{repo_id} read"
942            )));
943        }
944        let oid = lower_hex(oid);
945        let key = lfs_media_key(&oid).map_err(|e| LfsAdapterError::Invalid(e.to_string()))?;
946        let meta = self
947            .ops
948            .store()
949            .stat(key.key_bytes().as_slice())
950            .await
951            .map_err(|e| LfsAdapterError::Storage(e.to_string()))?;
952        let entry_len = match meta {
953            Some(meta) => meta.len,
954            None => {
955                return Err(LfsAdapterError::Storage(
956                    "the media entry is absent".to_owned(),
957                ))
958            }
959        };
960        let (from, until) = match range {
961            None => (0, entry_len),
962            Some(range) => {
963                if range.offset >= entry_len {
964                    return Err(LfsAdapterError::NotSatisfiable);
965                }
966                let end = match range.len {
967                    Some(len) => range.offset.saturating_add(len).min(entry_len),
968                    None => entry_len,
969                };
970                (range.offset, end)
971            }
972        };
973        if until - from > self.config.max_object_bytes {
974            return Err(LfsAdapterError::TooLarge(entry_len));
975        }
976        let whole_span = from == 0 && until == entry_len;
977        Ok(LfsDownload::start(
978            Arc::clone(self.ops.store()),
979            key,
980            from,
981            until,
982            self.config.window_bytes,
983            whole_span,
984        ))
985    }
986}
987
988/// The probe's outcome (the control plane's answer, folded to what
989/// the batch branch consumes).
990enum StatProbe {
991    /// The formed key is absent from the pool (`BLOB_MISSING`).
992    Missing,
993    /// The entry's length (`blobs/stat`'s `len`).
994    Length(u64),
995    /// The ACL refused (a raced revocation mid-batch).
996    Forbidden,
997    /// Any other failure.
998    Failed,
999}
1000
1001/// The lowercase-hex normalization (the validated oid form).
1002fn lower_hex(oid: &str) -> String {
1003    oid.chars().map(|c| c.to_ascii_lowercase()).collect()
1004}
1005
1006/// The digest's hex form over the key's physical bytes (tag-prefixed
1007/// key hex — the ops wire form, ADR-020 §Decision 2).
1008fn key_hex(key: &Key) -> String {
1009    let bytes = key.as_bytes();
1010    let mut hex = String::with_capacity(bytes.len() * 2);
1011    for byte in &bytes {
1012        hex.push(char::from_digit((byte >> 4) as u32, 16).unwrap_or('0'));
1013        hex.push(char::from_digit((byte & 0xf) as u32, 16).unwrap_or('0'));
1014    }
1015    hex
1016}
1017
1018/// The byte-slice's hex form (the digest comparisons).
1019fn hex_of(bytes: &[u8]) -> String {
1020    let mut hex = String::with_capacity(bytes.len() * 2);
1021    for byte in bytes {
1022        hex.push(char::from_digit((byte >> 4) as u32, 16).unwrap_or('0'));
1023        hex.push(char::from_digit((byte & 0xf) as u32, 16).unwrap_or('0'));
1024    }
1025    hex
1026}
1027
1028/// The identity requirement — the adapter never synthesizes one (the
1029/// `anon` composition is the door's; lfs.md §1).
1030fn require_identity(identity: Option<&Identity>) -> Result<Identity, LfsAdapterError> {
1031    identity
1032        .cloned()
1033        .ok_or_else(|| LfsAdapterError::Forbidden("authentication required".to_owned()))
1034}
1035
1036/// The pre-keyed put's machinery — the put handler's contract minus
1037/// the channel, replicated in-process: the facade put consumes a
1038/// bounded pipe whose feed is the caller body's one read; the sha256
1039/// pass rides the same read; a claim the derivation contradicts
1040/// kills the put pre-EOF (the kill's ordering — flag armed ahead of
1041/// the sender's drop — matches the put handler's pipe writer). The
1042/// facade arm is `put_as(GitObjectSha256, Some(len), …)` — the
1043/// identity family's derivation is the raw content's hash (the oid,
1044/// byte-exact), so the returned key equals the formed key on an
1045/// honest body and the cross-check is the same one-stream pass.
1046async fn pre_keyed_put<R>(
1047    store: Arc<BlobStore>,
1048    expected: &Key,
1049    oid: &str,
1050    size: u64,
1051    body: R,
1052) -> Result<(Key, Pin), LfsAdapterError>
1053where
1054    R: AsyncRead + Unpin + Send,
1055{
1056    const PIPE_BOUND: usize = 64;
1057    const READ_CHUNK: usize = 16 * 1024;
1058
1059    enum StreamEnd {
1060        Eof,
1061        BodyFailed(tokio::io::Error),
1062        PutEnded,
1063    }
1064
1065    let fail = Arc::new(AtomicBool::new(false));
1066    let (tx, rx) = mpsc::channel::<Arc<Vec<u8>>>(PIPE_BOUND);
1067    let put = {
1068        let store = Arc::clone(&store);
1069        let fail = Arc::clone(&fail);
1070        tokio::spawn(async move {
1071            store
1072                .put_as(
1073                    alkblobs::Algorithm::GitObjectSha256,
1074                    Some(size),
1075                    PutPipe {
1076                        rx,
1077                        fail,
1078                        buffer: None,
1079                        offset: 0,
1080                    },
1081                )
1082                .await
1083        })
1084    };
1085
1086    let mut body = body;
1087    let mut hasher = Sha256::new();
1088    let mut window = vec![0u8; READ_CHUNK];
1089    let end = loop {
1090        if put.is_finished() {
1091            break StreamEnd::PutEnded;
1092        }
1093        match body.read(&mut window).await {
1094            Ok(0) => break StreamEnd::Eof,
1095            Ok(n) => {
1096                hasher.update(&window[..n]);
1097                if tx.send(Arc::new(window[..n].to_vec())).await.is_err() {
1098                    break StreamEnd::PutEnded;
1099                }
1100            }
1101            Err(e) => break StreamEnd::BodyFailed(e),
1102        }
1103    };
1104
1105    match end {
1106        StreamEnd::Eof => {
1107            let derived = hex_of(&hasher.finalize());
1108            if derived != oid {
1109                fail.store(true, Ordering::Release);
1110                drop(tx);
1111                let _ = put.await;
1112                return Err(LfsAdapterError::MislabeledBody(format!(
1113                    "no stored content, no held pin — {oid}"
1114                )));
1115            }
1116            drop(tx);
1117            match put.await {
1118                Ok(Ok(landing)) => {
1119                    if landing.0 != *expected {
1120                        return Err(LfsAdapterError::MislabeledBody(oid.to_owned()));
1121                    }
1122                    Ok(landing)
1123                }
1124                Ok(Err(e)) => Err(LfsAdapterError::Storage(e.to_string())),
1125                Err(e) => Err(LfsAdapterError::Storage(format!(
1126                    "the put's store task failed: {e}"
1127                ))),
1128            }
1129        }
1130        StreamEnd::BodyFailed(e) => {
1131            fail.store(true, Ordering::Release);
1132            drop(tx);
1133            let _ = put.await;
1134            Err(LfsAdapterError::Storage(format!(
1135                "the upload body failed mid-stream: {e}"
1136            )))
1137        }
1138        StreamEnd::PutEnded => match put.await {
1139            Ok(Ok(_)) => Err(LfsAdapterError::Storage(
1140                "the put's store arm completed against a torn stream".to_owned(),
1141            )),
1142            Ok(Err(e)) => Err(LfsAdapterError::Storage(e.to_string())),
1143            Err(e) => Err(LfsAdapterError::Storage(format!(
1144                "the put's store task failed: {e}"
1145            ))),
1146        },
1147    }
1148}
1149
1150/// The put pipe's reader half (the put handler's `PutPipeReader`
1151/// shape): the facade put consumes the tee's feed; the kill flag
1152/// arms the next read into an error — the facade's stream-error path
1153/// (no commit, zero residue).
1154struct PutPipe {
1155    rx: mpsc::Receiver<Arc<Vec<u8>>>,
1156    fail: Arc<AtomicBool>,
1157    buffer: Option<Arc<Vec<u8>>>,
1158    offset: usize,
1159}
1160
1161impl AsyncRead for PutPipe {
1162    fn poll_read(
1163        self: std::pin::Pin<&mut Self>,
1164        cx: &mut PollContext<'_>,
1165        buf: &mut ReadBuf<'_>,
1166    ) -> Poll<tokio::io::Result<()>> {
1167        let this = self.get_mut();
1168        loop {
1169            if this.fail.load(Ordering::Acquire) {
1170                return Poll::Ready(Err(tokio::io::Error::new(
1171                    tokio::io::ErrorKind::BrokenPipe,
1172                    "the put's feed failed pre-commit (the stream errored)",
1173                )));
1174            }
1175            if let Some(chunk) = &this.buffer {
1176                if this.offset < chunk.len() {
1177                    let n = (chunk.len() - this.offset).min(buf.remaining());
1178                    buf.put_slice(&chunk[this.offset..this.offset + n]);
1179                    this.offset += n;
1180                    return Poll::Ready(Ok(()));
1181                }
1182                this.buffer = None;
1183                this.offset = 0;
1184            }
1185            match this.rx.poll_recv(cx) {
1186                Poll::Ready(Some(chunk)) => {
1187                    this.buffer = Some(chunk);
1188                    this.offset = 0;
1189                }
1190                Poll::Ready(None) => return Poll::Ready(Ok(())),
1191                Poll::Pending => return Poll::Pending,
1192            }
1193        }
1194    }
1195}
1196
1197/// The download's verified-window stream — an [`AsyncRead`] fed by a
1198/// pump that splits the requested range at the configured window
1199/// size and yields each window only after its mid-flight slice-digest
1200/// check passes (the module doc's byte-path section) — ADR-022 §2
1201/// as amended (the download byte path's verification posture). A failed
1202/// window check or a failed read surfaces as an io error at the
1203/// yield point (already-delivered windows stand; the caller aborts
1204/// and re-fetches). The pump ends at EOF; a zero-length whole read
1205/// is an empty stream (the entry's zero-length entry served).
1206pub struct LfsDownload {
1207    rx: mpsc::Receiver<tokio::io::Result<Vec<u8>>>,
1208    buffer: Option<Vec<u8>>,
1209    position: usize,
1210}
1211
1212impl LfsDownload {
1213    /// Start the pump (the download call's tail). The channel bound
1214    /// is two windows — the store's pacing backpressures the
1215    /// consumer through the pipe (the same discipline as the put's
1216    /// bounded pipe, read-swapped). A whole-span download
1217    /// (`whole_span`) rides the end-to-end oid digest check.
1218    fn start(
1219        store: Arc<BlobStore>,
1220        key: Key,
1221        from: u64,
1222        until: u64,
1223        window_bytes: u64,
1224        whole_span: bool,
1225    ) -> Self {
1226        let (tx, rx) = mpsc::channel(2);
1227        tokio::spawn(verified_windows(
1228            store,
1229            key,
1230            from,
1231            until,
1232            window_bytes,
1233            whole_span,
1234            tx,
1235        ));
1236        LfsDownload {
1237            rx,
1238            buffer: None,
1239            position: 0,
1240        }
1241    }
1242}
1243
1244impl AsyncRead for LfsDownload {
1245    fn poll_read(
1246        self: std::pin::Pin<&mut Self>,
1247        cx: &mut PollContext<'_>,
1248        buf: &mut ReadBuf<'_>,
1249    ) -> Poll<tokio::io::Result<()>> {
1250        let this = self.get_mut();
1251        loop {
1252            if let Some(chunk) = this.buffer.as_ref() {
1253                if this.position < chunk.len() {
1254                    let n = (chunk.len() - this.position).min(buf.remaining());
1255                    buf.put_slice(&chunk[this.position..this.position + n]);
1256                    this.position += n;
1257                    return Poll::Ready(Ok(()));
1258                }
1259                this.buffer = None;
1260                this.position = 0;
1261            }
1262            match this.rx.poll_recv(cx) {
1263                Poll::Ready(Some(Ok(chunk))) => {
1264                    this.buffer = Some(chunk);
1265                    this.position = 0;
1266                }
1267                Poll::Ready(Some(Err(e))) => return Poll::Ready(Err(e)),
1268                Poll::Ready(None) => return Poll::Ready(Ok(())),
1269                Poll::Pending => return Poll::Pending,
1270            }
1271        }
1272    }
1273}
1274
1275/// The download pump: one `read_range` per window (clamped at the
1276/// requested end), the slice digest checked against the sha256 of
1277/// exactly the returned bytes mid-flight, then the window handed on.
1278/// A whole-span download (`whole_span`) additionally accumulates the
1279/// windows' bytes into one sha256 pass and checks the finished
1280/// digest against the key's derivation digest **before the last
1281/// window yields** — the identity family's digest is the raw
1282/// content's hash, so the comparison is the end-to-end oid
1283/// verification over the large tier's unverified reads (the
1284/// module doc's verification statement; the wave-9 gate's fix —
1285/// the per-window slice pair was checked against the same in-process
1286/// bytes and could not catch a corrupted entry).
1287async fn verified_windows(
1288    store: Arc<BlobStore>,
1289    key: Key,
1290    from: u64,
1291    until: u64,
1292    window_bytes: u64,
1293    whole_span: bool,
1294    tx: mpsc::Sender<tokio::io::Result<Vec<u8>>>,
1295) {
1296    let key_bytes = key.key_bytes();
1297    let expected = whole_span.then(|| key.digest().bytes().to_vec());
1298    let mut hasher = whole_span.then(Sha256::new);
1299    let mut offset = from;
1300    while offset < until {
1301        let window_len = window_bytes.min(until - offset);
1302        let last = offset + window_len == until;
1303        match store
1304            .read_range(key_bytes.as_slice(), offset, window_len)
1305            .await
1306        {
1307            Ok((slice, digest)) => {
1308                let hash = Sha256::digest(&slice);
1309                if slice.len() as u64 != window_len || hex_of(hash.as_slice()) != hex_of(&digest) {
1310                    let _ = tx
1311                        .send(Err(tokio::io::Error::other(
1312                            "the verified window's slice digest mismatched mid-flight",
1313                        )))
1314                        .await;
1315                    return;
1316                }
1317                if let Some(h) = hasher.as_mut() {
1318                    h.update(&slice);
1319                }
1320                if last && whole_span {
1321                    if let (Some(h), Some(expected_bytes)) = (hasher.as_ref(), expected.as_deref())
1322                    {
1323                        let digest_now = h.clone().finalize();
1324                        if digest_now.as_slice() != expected_bytes {
1325                            let _ = tx
1326                                .send(Err(tokio::io::Error::other(
1327                                    "the served media's whole-content digest mismatched the oid",
1328                                )))
1329                                .await;
1330                            return;
1331                        }
1332                    }
1333                }
1334                offset += window_len;
1335                if tx.send(Ok(slice)).await.is_err() {
1336                    return;
1337                }
1338            }
1339            Err(e) => {
1340                let _ = tx
1341                    .send(Err(tokio::io::Error::other(format!(
1342                        "the media read failed: {e}"
1343                    ))))
1344                    .await;
1345                return;
1346            }
1347        }
1348    }
1349}
1350
1351#[cfg(test)]
1352mod battery {
1353    use std::collections::HashMap;
1354
1355    use tokio::io::AsyncReadExt;
1356
1357    use alkgit::{AccessAction, RepoRecord, Visibility};
1358
1359    use super::*;
1360    use crate::registry::{CachedRegistryProvider, RegistryRow};
1361
1362    fn oid_of(content: &[u8]) -> String {
1363        let digest = Sha256::digest(content);
1364        hex_of(digest.as_slice())
1365    }
1366
1367    fn identity(id: &str) -> Identity {
1368        Identity {
1369            id: id.to_owned(),
1370            scopes: Vec::new(),
1371            resources: HashMap::new(),
1372        }
1373    }
1374
1375    /// An identity carrying a **static** `Identity.resources` claim on
1376    /// the ops resource — the shape the gateway-gap regression pins
1377    /// (admitted only if the wired provider grants; the static path
1378    /// is never consulted through the adapter).
1379    fn identity_with_static_claim(id: &str) -> Identity {
1380        let mut resources = HashMap::new();
1381        resources.insert(
1382            "blob-namespace".to_owned(),
1383            vec!["read".to_owned(), "write".to_owned()],
1384        );
1385        Identity {
1386            id: id.to_owned(),
1387            scopes: Vec::new(),
1388            resources,
1389        }
1390    }
1391
1392    fn record(
1393        repo_id: &str,
1394        visibility: Visibility,
1395        grants: &[(&str, &[AccessAction])],
1396    ) -> RepoRecord {
1397        RepoRecord {
1398            repo_id: repo_id.to_owned(),
1399            visibility,
1400            grants: grants
1401                .iter()
1402                .map(|(id, actions)| ((*id).to_owned(), actions.to_vec()))
1403                .collect(),
1404        }
1405    }
1406
1407    fn registry_provider() -> Arc<CachedRegistryProvider> {
1408        let provider = Arc::new(CachedRegistryProvider::new());
1409        provider.install(vec![
1410            RegistryRow {
1411                record: record(
1412                    "owner/granted",
1413                    Visibility::Private,
1414                    &[("u-writer", &[AccessAction::Read, AccessAction::Write])],
1415                ),
1416                seq: 1,
1417            },
1418            RegistryRow {
1419                record: record(
1420                    "owner/readonly",
1421                    Visibility::Private,
1422                    &[
1423                        ("u-writer", &[AccessAction::Read, AccessAction::Write]),
1424                        ("u-reader", &[AccessAction::Read]),
1425                    ],
1426                ),
1427                seq: 2,
1428            },
1429            RegistryRow {
1430                record: record(
1431                    "owner/public",
1432                    Visibility::Public,
1433                    &[("u-writer", &[AccessAction::Read, AccessAction::Write])],
1434                ),
1435                seq: 3,
1436            },
1437            RegistryRow {
1438                record: record("owner/closed", Visibility::Private, &[]),
1439                seq: 4,
1440            },
1441        ]);
1442        provider
1443    }
1444
1445    fn adapter(
1446        provider: Arc<dyn OwnershipProvider>,
1447        config: LfsAdapterConfig,
1448    ) -> (LfsAdapter, Arc<BlobStore>, Arc<crate::lfs::InMemoryLfsRows>) {
1449        let rows = Arc::new(crate::lfs::InMemoryLfsRows::new());
1450        let store = Arc::new(BlobStore::mem_only());
1451        let adapter = LfsAdapter::new(Arc::clone(&store), rows.clone(), provider, config).unwrap();
1452        (adapter, store, rows)
1453    }
1454
1455    fn grant_provider() -> Arc<dyn OwnershipProvider> {
1456        Arc::new(LfsGrantProvider::new(registry_provider()))
1457    }
1458
1459    fn object(oid: &str, size: u64) -> LfsBatchObject {
1460        LfsBatchObject {
1461            oid: oid.to_owned(),
1462            size,
1463        }
1464    }
1465
1466    fn upload_request(objects: Vec<LfsBatchObject>) -> LfsBatchRequest {
1467        LfsBatchRequest {
1468            operation: LfsBatchOperation::Upload,
1469            hash_algo: None,
1470            objects,
1471        }
1472    }
1473
1474    fn download_request(objects: Vec<LfsBatchObject>) -> LfsBatchRequest {
1475        LfsBatchRequest {
1476            operation: LfsBatchOperation::Download,
1477            hash_algo: None,
1478            objects,
1479        }
1480    }
1481
1482    async fn upload_media(
1483        adapter: &LfsAdapter,
1484        repo_id: &str,
1485        content: &[u8],
1486    ) -> Result<(), LfsAdapterError> {
1487        adapter
1488            .upload(
1489                Some(&identity("u-writer")),
1490                repo_id,
1491                &oid_of(content),
1492                Some(content.len() as u64),
1493                content,
1494            )
1495            .await
1496    }
1497
1498    async fn read_all(mut download: LfsDownload) -> Vec<u8> {
1499        let mut out = Vec::new();
1500        download
1501            .read_to_end(&mut out)
1502            .await
1503            .expect("the download reads");
1504        out
1505    }
1506
1507    async fn download_whole(adapter: &LfsAdapter, repo_id: &str, oid: &str) -> Vec<u8> {
1508        read_all(
1509            adapter
1510                .download(Some(&identity("u-writer")), repo_id, oid, None)
1511                .await
1512                .unwrap(),
1513        )
1514        .await
1515    }
1516
1517    /// The writer identity's batch upload: the two absent objects
1518    /// answer with upload mint inputs (no URLs — the inputs are the
1519    /// repo/oid pair), the rows are asserted at check time, the byte
1520    /// path stores exact bytes, the re-push answers no actions, the
1521    /// dedup re-put is idempotent, and the custody consumes the
1522    /// minted mapping at confirm.
1523    #[tokio::test]
1524    async fn the_upload_batch_and_the_pre_keyed_put() {
1525        let (adapter, store, rows) = adapter(grant_provider(), LfsAdapterConfig::default());
1526        let writer = identity("u-writer");
1527        let content_a = b"the first LFS media\n".to_vec();
1528        let content_b = b"the second LFS media\n".to_vec();
1529        let (oid_a, oid_b) = (oid_of(&content_a), oid_of(&content_b));
1530
1531        let answer = adapter
1532            .check(
1533                Some(&writer),
1534                "owner/granted",
1535                upload_request(vec![object(&oid_a, content_a.len() as u64)]),
1536            )
1537            .await
1538            .unwrap();
1539        assert_eq!(answer.transfer, "basic");
1540        assert_eq!(answer.hash_algo, "sha256");
1541        let first = &answer.objects[0];
1542        assert_eq!(first.oid, oid_a);
1543        assert!(first.authenticated);
1544        assert!(first.error.is_none());
1545        let mint = first
1546            .actions
1547            .as_ref()
1548            .and_then(|a| a.upload.as_ref())
1549            .expect("the upload mint input");
1550        assert_eq!(mint.repo_id, "owner/granted");
1551        assert_eq!(mint.oid, oid_a);
1552        assert!(first
1553            .actions
1554            .as_ref()
1555            .and_then(|a| a.download.as_ref())
1556            .is_none());
1557        // the row asserted at check time (the moment the server learns the pair)
1558        let asserted = rows.repo_rows("owner/granted").await.unwrap();
1559        assert_eq!(
1560            asserted,
1561            vec![crate::lfs::LfsRow {
1562                repo_id: "owner/granted".to_owned(),
1563                oid: oid_a.clone(),
1564                size: content_a.len() as u64,
1565                refcount: 1,
1566            }]
1567        );
1568
1569        // the byte path: exact bytes in, exact bytes out.
1570        upload_media(&adapter, "owner/granted", &content_a)
1571            .await
1572            .unwrap();
1573        assert_eq!(
1574            download_whole(&adapter, "owner/granted", &oid_a).await,
1575            content_a
1576        );
1577        let stat = rows.repo_rows("owner/granted").await.unwrap();
1578        assert_eq!(
1579            stat.iter().find(|r| r.oid == oid_a).unwrap().size,
1580            content_a.len() as u64,
1581            "the upload refreshed the assertion"
1582        );
1583
1584        // the re-push: the present object answers no actions (the
1585        // idempotent posture) — and the batch with both objects
1586        // still mints the absent one.
1587        let answer = adapter
1588            .check(
1589                Some(&writer),
1590                "owner/granted",
1591                upload_request(vec![
1592                    object(&oid_a, content_a.len() as u64),
1593                    object(&oid_b, content_b.len() as u64),
1594                ]),
1595            )
1596            .await
1597            .unwrap();
1598        assert!(
1599            answer.objects[0].actions.is_none(),
1600            "the present object: nothing to do"
1601        );
1602        assert!(answer.objects[0].error.is_none());
1603        assert!(
1604            answer.objects[1]
1605                .actions
1606                .as_ref()
1607                .and_then(|a| a.upload.as_ref())
1608                .is_some(),
1609            "the absent object still offers the upload mint"
1610        );
1611
1612        // the dedup re-put lands (the CAS-idempotent posture).
1613        upload_media(&adapter, "owner/granted", &content_a)
1614            .await
1615            .unwrap();
1616
1617        // the confirm retired the minted mapping (the custody consumed;
1618        // the entry stands, protected by the registered rows source).
1619        assert_eq!(adapter.ops.tokens().len(), 0);
1620        let key = crate::liveness::lfs_media_key(&oid_a).unwrap();
1621        assert!(
1622            store
1623                .stat(key.key_bytes().as_slice())
1624                .await
1625                .unwrap()
1626                .is_some(),
1627            "the committed content stands post-confirm"
1628        );
1629    }
1630
1631    /// The download batch's shapes: the present object answers the
1632    /// download mint input, the absent object answers the 404 error
1633    /// object, the size-disagreeing request answers the 422, and the
1634    /// anonymous-public read composes through the door's synthesized
1635    /// `anon` identity (read-only on the public repo).
1636    #[tokio::test]
1637    async fn the_download_batch_shapes() {
1638        let (adapter, _store, _rows) = adapter(grant_provider(), LfsAdapterConfig::default());
1639        let content = b"the downloadable media\n".to_vec();
1640        let oid = oid_of(&content);
1641        upload_media(&adapter, "owner/public", &content)
1642            .await
1643            .unwrap();
1644
1645        let answer = adapter
1646            .check(
1647                Some(&identity("anon")),
1648                "owner/public",
1649                download_request(vec![
1650                    object(&oid, content.len() as u64),
1651                    object(&oid_of(b"never stored"), 10),
1652                ]),
1653            )
1654            .await
1655            .unwrap();
1656        let present = &answer.objects[0];
1657        assert!(present.authenticated);
1658        assert!(present.error.is_none());
1659        let mint = present
1660            .actions
1661            .as_ref()
1662            .and_then(|a| a.download.as_ref())
1663            .expect("the download mint input");
1664        assert_eq!(mint.repo_id, "owner/public");
1665        assert_eq!(mint.oid, oid);
1666
1667        let absent = &answer.objects[1];
1668        assert_eq!(absent.error.as_ref().unwrap().code, 404);
1669        assert!(absent.actions.is_none());
1670
1671        // the anonymous upload batch is refused (push is always
1672        // authenticated) and the anonymous read of a private repo is
1673        // refused (unknown ≡ unauthorized).
1674        assert!(matches!(
1675            adapter
1676                .check(
1677                    Some(&identity("anon")),
1678                    "owner/public",
1679                    upload_request(vec![object(&oid, 10)])
1680                )
1681                .await,
1682            Err(LfsAdapterError::Forbidden(_))
1683        ));
1684        assert!(matches!(
1685            adapter
1686                .check(
1687                    Some(&identity("anon")),
1688                    "owner/closed",
1689                    download_request(vec![object(&oid, 10)])
1690                )
1691                .await,
1692            Err(LfsAdapterError::Forbidden(_))
1693        ));
1694
1695        // the size disagreement (the pointer's size contradicts the
1696        // stored content's length) answers the 422 error object.
1697        let answer = adapter
1698            .check(
1699                Some(&identity("u-writer")),
1700                "owner/granted",
1701                download_request(vec![object(&oid, content.len() as u64 + 5)]),
1702            )
1703            .await
1704            .unwrap();
1705        assert_eq!(answer.objects[0].error.as_ref().unwrap().code, 422);
1706        assert_eq!(
1707            answer.objects[0].error.as_ref().unwrap().message,
1708            "size disagreement"
1709        );
1710    }
1711
1712    /// The pinned protocol gates: `hash_algo` serves sha256 only
1713    /// (the 409 fact) and the size-less upload offer is refused.
1714    #[tokio::test]
1715    async fn the_hash_algo_gate_and_the_sizeless_refusal() {
1716        let (adapter, _store, rows) = adapter(grant_provider(), LfsAdapterConfig::default());
1717        let writer = identity("u-writer");
1718        let oid = oid_of(b"gate media");
1719
1720        let mut request = upload_request(vec![object(&oid, 10)]);
1721        request.hash_algo = Some("sha1".to_owned());
1722        assert!(
1723            matches!(
1724                adapter.check(Some(&writer), "owner/granted", request).await,
1725                Err(LfsAdapterError::HashAlgo(algo)) if algo == "sha1"
1726            ),
1727            "a non-sha256 batch is the pinned 409"
1728        );
1729        assert!(
1730            rows.repo_rows("owner/granted").await.unwrap().is_empty(),
1731            "the refused batch asserted nothing"
1732        );
1733
1734        // the default algorithm (`None`) and the explicit sha256 pass;
1735        // their checks assert the pair (the row assertion's own contract).
1736        let mut request = upload_request(vec![object(&oid, 10)]);
1737        request.hash_algo = Some("sha256".to_owned());
1738        adapter
1739            .check(Some(&writer), "owner/granted", request)
1740            .await
1741            .unwrap();
1742        adapter
1743            .check(
1744                Some(&writer),
1745                "owner/granted",
1746                upload_request(vec![object(&oid, 10)]),
1747            )
1748            .await
1749            .unwrap();
1750        assert_eq!(rows.repo_rows("owner/granted").await.unwrap().len(), 1);
1751
1752        assert!(
1753            matches!(
1754                adapter
1755                    .upload(Some(&writer), "owner/granted", &oid, None, &b"media"[..])
1756                    .await,
1757                Err(LfsAdapterError::SizelessOffer)
1758            ),
1759            "the size-less offer is a protocol error"
1760        );
1761        assert_eq!(
1762            rows.repo_rows("owner/granted").await.unwrap().len(),
1763            1,
1764            "the size-less refusal asserted nothing"
1765        );
1766    }
1767
1768    /// The mislabeled body: the put's contract — the claim
1769    /// contradicted by the derivation kills the put pre-EOF (zero
1770    /// residue: no stored content, no held pin), the row assertion
1771    /// stands, and a corrected re-upload lands.
1772    #[tokio::test]
1773    async fn the_mislabeled_body_refusal() {
1774        let (adapter, store, rows) = adapter(grant_provider(), LfsAdapterConfig::default());
1775        let claimed = oid_of(b"the honest content");
1776        let honest = b"the honest content".to_vec();
1777
1778        upload_media(&adapter, "owner/granted", &honest)
1779            .await
1780            .unwrap();
1781        let answer = adapter
1782            .download(Some(&identity("u-writer")), "owner/granted", &claimed, None)
1783            .await
1784            .unwrap();
1785        assert_eq!(read_all(answer).await, honest);
1786
1787        // a body whose sha256 is not the claimed oid — the put kills
1788        // pre-EOF and nothing stores.
1789        let bad = b"the dishonest content".to_vec();
1790        adapter
1791            .upload(
1792                Some(&identity("u-writer")),
1793                "owner/granted",
1794                &claimed,
1795                Some(bad.len() as u64),
1796                bad.as_slice(),
1797            )
1798            .await
1799            .unwrap_err();
1800
1801        let key = crate::liveness::lfs_media_key(&claimed).unwrap();
1802        let key_borrow = key.key_bytes();
1803        assert_eq!(
1804            store.stat(&key_borrow).await.unwrap().unwrap().len,
1805            honest.len() as u64,
1806            "the honest entry is the committed one — nothing mislabeled landed"
1807        );
1808        let list = store.list().await.unwrap();
1809        assert_eq!(list.len(), 1, "the kill left zero residue");
1810        assert_eq!(
1811            rows.repo_rows("owner/granted").await.unwrap()[0].oid,
1812            claimed
1813        );
1814        assert_eq!(
1815            adapter.ops.tokens().len(),
1816            0,
1817            "no held pin, no mapping left"
1818        );
1819
1820        // a short body against the offer's len — the facade's own
1821        // known-length refusal (zero residue), the put contract's
1822        // inherited posture.
1823        adapter
1824            .upload(
1825                Some(&identity("u-writer")),
1826                "owner/granted",
1827                &oid_of(&bad),
1828                Some(bad.len() as u64 + 10),
1829                &bad[..bad.len() - 5],
1830            )
1831            .await
1832            .unwrap_err();
1833        assert_eq!(store.list().await.unwrap().len(), 1);
1834    }
1835
1836    /// The ranged download: windows split at the configured boundary,
1837    /// each verified mid-flight, byte-exact over the whole and every
1838    /// slice; the past-EOF offset is the not-satisfiable refusal and
1839    /// the whole download honors the object-size ceiling.
1840    #[tokio::test]
1841    async fn the_ranged_download_splits_and_verifies() {
1842        let config = LfsAdapterConfig {
1843            window_bytes: 1024,
1844            max_object_bytes: 8 * 1024,
1845        };
1846        let (adapter, _store, _rows) = adapter(grant_provider(), config);
1847        let mut content = Vec::new();
1848        for i in 0..100u32 {
1849            content.extend_from_slice(format!("{i:064x}").as_bytes());
1850        }
1851        let oid = oid_of(&content);
1852        upload_media(&adapter, "owner/granted", &content)
1853            .await
1854            .unwrap();
1855
1856        // whole download (over the window boundary — the adapter's
1857        // window arithmetic; byte-exact across the verified windows).
1858        assert_eq!(
1859            download_whole(&adapter, "owner/granted", &oid).await,
1860            content
1861        );
1862
1863        // a slice, byte-exact.
1864        let slice = adapter
1865            .download(
1866                Some(&identity("u-writer")),
1867                "owner/granted",
1868                &oid,
1869                Some(LfsRange {
1870                    offset: 500,
1871                    len: Some(1000),
1872                }),
1873            )
1874            .await
1875            .unwrap();
1876        assert_eq!(read_all(slice).await, content[500..1500]);
1877
1878        // an open-ended slice (to the end).
1879        let tail = adapter
1880            .download(
1881                Some(&identity("u-writer")),
1882                "owner/granted",
1883                &oid,
1884                Some(LfsRange {
1885                    offset: content.len() as u64 - 100,
1886                    len: None,
1887                }),
1888            )
1889            .await
1890            .unwrap();
1891        assert_eq!(read_all(tail).await, content[content.len() - 100..]);
1892
1893        // past-EOF offset: the not-satisfiable refusal.
1894        assert!(matches!(
1895            adapter
1896                .download(
1897                    Some(&identity("u-writer")),
1898                    "owner/granted",
1899                    &oid,
1900                    Some(LfsRange {
1901                        offset: content.len() as u64 + 1,
1902                        len: None,
1903                    })
1904                )
1905                .await,
1906            Err(LfsAdapterError::NotSatisfiable)
1907        ));
1908
1909        // a whole download over the ceiling is refused; the
1910        // full-span ranged form of the same object is the same span
1911        // and refuses too (the ceiling counts the resolved span, not
1912        // the request's header form); a partial span under the
1913        // ceiling rides the window budget.
1914        let big = vec![0u8; 10 * 1024];
1915        let big_oid = oid_of(&big);
1916        upload_media(&adapter, "owner/granted", &big).await.unwrap();
1917        assert!(matches!(
1918            adapter
1919                .download(Some(&identity("u-writer")), "owner/granted", &big_oid, None)
1920                .await,
1921            Err(LfsAdapterError::TooLarge(_))
1922        ));
1923        assert!(matches!(
1924            adapter
1925                .download(
1926                    Some(&identity("u-writer")),
1927                    "owner/granted",
1928                    &big_oid,
1929                    Some(LfsRange {
1930                        offset: 0,
1931                        len: Some(10 * 1024),
1932                    }),
1933                )
1934                .await,
1935            Err(LfsAdapterError::TooLarge(_))
1936        ));
1937        let ranged = adapter
1938            .download(
1939                Some(&identity("u-writer")),
1940                "owner/granted",
1941                &big_oid,
1942                Some(LfsRange {
1943                    offset: 0,
1944                    len: Some(4 * 1024),
1945                }),
1946            )
1947            .await
1948            .unwrap();
1949        assert_eq!(read_all(ranged).await, big[..4 * 1024]);
1950
1951        // the reader identity rides the read grant; the unknown oid
1952        // is the absent-entry refusal.
1953        assert!(matches!(
1954            adapter
1955                .download(
1956                    Some(&identity("u-writer")),
1957                    "owner/granted",
1958                    &oid_of(b"absent"),
1959                    None
1960                )
1961                .await,
1962            Err(LfsAdapterError::Storage(_))
1963        ));
1964    }
1965
1966    /// The wired-provider posture (the POC-L gateway-gap regression,
1967    /// re-landed): the static `Identity.resources` fallback is never
1968    /// consulted through the adapter — a claim-carrying identity is
1969    /// admitted only through the wired provider's live grants; the
1970    /// read-only grantee downloads but never uploads; the grant
1971    /// monotonicity (manage ⊃ write ⊃ read) and the unknown-repo
1972    /// denial ride the same `authorize` policy the git surface
1973    /// composes.
1974    #[tokio::test]
1975    async fn the_wired_provider_posture() {
1976        let provider = grant_provider();
1977        let (adapter, _store, _rows) = adapter(provider.clone(), LfsAdapterConfig::default());
1978        let content = b"the gated media\n".to_vec();
1979        let oid = oid_of(&content);
1980
1981        // no identity: the adapter never synthesizes one.
1982        assert!(matches!(
1983            adapter
1984                .check(
1985                    None,
1986                    "owner/granted",
1987                    download_request(vec![object(&oid, 10)])
1988                )
1989                .await,
1990            Err(LfsAdapterError::Forbidden(_))
1991        ));
1992
1993        // the unrecognized identity: refused everywhere, regardless
1994        // of a static resources claim (the fallback is unreachable —
1995        // the contexts always wire the provider).
1996        for identity in [identity("u-ghost"), identity_with_static_claim("u-ghost")] {
1997            assert!(matches!(
1998                adapter
1999                    .check(
2000                        Some(&identity),
2001                        "owner/closed",
2002                        download_request(vec![object(&oid, 10)])
2003                    )
2004                    .await,
2005                Err(LfsAdapterError::Forbidden(_))
2006            ));
2007            assert!(matches!(
2008                adapter
2009                    .upload(Some(&identity), "owner/closed", &oid, Some(10), &b"x"[..])
2010                    .await,
2011                Err(LfsAdapterError::Forbidden(_))
2012            ));
2013            assert!(matches!(
2014                adapter
2015                    .download(Some(&identity), "owner/closed", &oid, None)
2016                    .await,
2017                Err(LfsAdapterError::Forbidden(_))
2018            ));
2019        }
2020
2021        // the static claim does not admit either: the same
2022        // claim-carrying identity through the granting repo — the
2023        // live grant table (the wired provider's answer) decides.
2024        let claimed = identity_with_static_claim("u-nobody");
2025        assert!(
2026            matches!(
2027                adapter
2028                    .check(
2029                        Some(&claimed),
2030                        "owner/granted",
2031                        download_request(vec![object(&oid, 10)])
2032                    )
2033                    .await,
2034                Err(LfsAdapterError::Forbidden(_))
2035            ),
2036            "a resources claim rides nothing when the provider refuses"
2037        );
2038
2039        // the read-only grantee: read granted, write refused — the
2040        // batch action mapping (lfs.md §1: read/write ↔
2041        // download/upload) enforced through the wired provider.
2042        let reader = identity("u-reader");
2043        upload_media(&adapter, "owner/readonly", &content)
2044            .await
2045            .unwrap();
2046        let answer = adapter
2047            .check(
2048                Some(&reader),
2049                "owner/readonly",
2050                download_request(vec![object(&oid, content.len() as u64)]),
2051            )
2052            .await
2053            .unwrap();
2054        assert!(answer.objects[0].error.is_none());
2055        assert!(answer.objects[0]
2056            .actions
2057            .as_ref()
2058            .unwrap()
2059            .download
2060            .is_some());
2061        assert!(matches!(
2062            adapter
2063                .check(
2064                    Some(&reader),
2065                    "owner/readonly",
2066                    upload_request(vec![object(&oid, 1)])
2067                )
2068                .await,
2069            Err(LfsAdapterError::Forbidden(_))
2070        ));
2071        let answer = adapter
2072            .download(Some(&reader), "owner/readonly", &oid, None)
2073            .await
2074            .unwrap();
2075        assert_eq!(read_all(answer).await, content);
2076
2077        // the provider's own rows: the public read (anon rides the
2078        // visibility, through the batch's provider), the denied
2079        // public write (push is always authenticated); the manage ⊃
2080        // write ⊃ read monotonicity, the unrecognized identity and
2081        // the unknown repo on a fresh provider.
2082        let anon = identity("anon");
2083        assert!(provider.owns(&anon, "blob-namespace", "owner/public", "read"));
2084        assert!(!provider.owns(&anon, "blob-namespace", "owner/public", "write"));
2085        let manages = record(
2086            "repo/m",
2087            Visibility::Private,
2088            &[("u-m", &[AccessAction::Manage])],
2089        );
2090        assert!(!manages.grants.is_empty());
2091        let provider2 = Arc::new(CachedRegistryProvider::new());
2092        provider2.install(vec![RegistryRow {
2093            record: manages,
2094            seq: 1,
2095        }]);
2096        let p: Arc<dyn OwnershipProvider> = Arc::new(LfsGrantProvider::new(provider2));
2097        // the ops resource actions are read/write — a stored manage
2098        // grant satisfies both (the authorize monotonicity); unknown
2099        // action strings deny; the unrecognized identity and the
2100        // unknown repo deny.
2101        assert!(p.owns(&identity("u-m"), "blob-namespace", "repo/m", "read"));
2102        assert!(p.owns(&identity("u-m"), "blob-namespace", "repo/m", "write"));
2103        assert!(!p.owns(&identity("u-x"), "blob-namespace", "repo/m", "read"));
2104        assert!(!p.owns(&identity("u-x"), "blob-namespace", "repo/m", "write"));
2105        assert!(!p.owns(&identity("u-m"), "blob-namespace", "repo/m", "manage"));
2106        assert!(!p.owns(&identity("u-m"), "blob-namespace", "repo/unknown", "read"));
2107        assert!(!p.owns(&identity("u-m"), "other-type", "repo/m", "read"));
2108        assert_eq!(
2109            p.owned_resources(&identity("u-m"), "blob-namespace"),
2110            vec!["repo/m".to_owned()]
2111        );
2112    }
2113
2114    /// The row assertion at check time, composed end-to-end: both
2115    /// arms assert, the malformed oid asserts nothing, the
2116    /// cross-reference composition bumps the rows' refcounts (the
2117    /// same oid referenced by two repos — the re-protection contract's
2118    /// data riding the adapter's assertions), and the re-assert is
2119    /// one row (the refcount-aware upsert).
2120    #[tokio::test]
2121    async fn the_rows_are_asserted_at_check_time() {
2122        let (adapter, _store, _rows) = adapter(grant_provider(), LfsAdapterConfig::default());
2123        let a = oid_of(b"shared media a");
2124        let malformed = "not-an-oid";
2125
2126        // upload arm, absent objects: asserted.
2127        adapter
2128            .check(
2129                Some(&identity("u-writer")),
2130                "owner/granted",
2131                upload_request(vec![object(&a, 15)]),
2132            )
2133            .await
2134            .unwrap();
2135        // download arm, absent objects: asserted too (the same posture).
2136        adapter
2137            .check(
2138                Some(&identity("u-writer")),
2139                "owner/readonly",
2140                download_request(vec![object(&a, 15), object(malformed, 15)]),
2141            )
2142            .await
2143            .unwrap();
2144
2145        let all = _rows.rows().await.unwrap();
2146        assert_eq!(all.len(), 2, "the malformed oid asserted nothing");
2147        for row in &all {
2148            assert_eq!(row.refcount, 2, "two repos — one key, two rows: {row:?}");
2149        }
2150
2151        // the re-assert refreshes the size; the refcount unmoved.
2152        adapter
2153            .check(
2154                Some(&identity("u-writer")),
2155                "owner/granted",
2156                upload_request(vec![object(&a, 15)]),
2157            )
2158            .await
2159            .unwrap();
2160        let all = _rows.rows().await.unwrap();
2161        let mine = all.iter().filter(|r| r.repo_id == "owner/granted").count();
2162        assert_eq!(mine, 1);
2163    }
2164}
2165
2166/// The pg fleet battery (F-S5's test half, the wave-10 booking): the
2167/// live-pg fleet assembly row — a kv-postgres **fleet** store under
2168/// the harness container, the adapter over it riding the live pg rows
2169/// twin — proving the fleet custody arm's confirm-immediately posture
2170/// consumes no admission slot across repeated uploads. The other
2171/// battery adapters are all `mem_only()` (the fleet owner resolves
2172/// `None`; only the custodial arm ever ran) — this is the fleet arm's
2173/// coverage: the fleet owner resolves (the arm's precondition), every
2174/// upload of fresh media succeeds (no `MintRefused`), the token
2175/// registry ends **empty** after every upload (each mint retired at
2176/// its immediate confirm — a dropped-unconfirmed token would hold its
2177/// admission slot to process end, the wave-9 gate's leak), the mint
2178/// boundary stays live per upload (an explicitly-held token persists
2179/// across an upload whose own mint lands and retires around it —
2180/// ADR-022 §5's hostile-loop posture, zero slot leak), and the pair
2181/// rows assert identically to the custodial arm's. Live-pg only;
2182/// clean skip when the harness env is absent (the `live_pg!`
2183/// convention).
2184#[cfg(all(test, feature = "postgres"))]
2185mod pg_fleet_tests {
2186    use std::collections::{BTreeMap, HashMap};
2187    use std::io::Cursor;
2188    use std::time::Duration;
2189
2190    use alkgit::{AccessAction, RepoRecord, Visibility};
2191
2192    use super::*;
2193    use crate::postgres::live_harness::{drop_schema, live_pg, unique_schema};
2194    use crate::registry::{CachedRegistryProvider, RegistryRow};
2195
2196    const REPO: &str = "owner/fleet-granted";
2197
2198    /// One fresh media body — the seed's little-endian form repeated,
2199    /// so every seed carries its own distinct header (each upload a
2200    /// new oid; the CAS re-put idempotence never degenerates the
2201    /// mints).
2202    fn media_of(seed: u64, len: usize) -> Vec<u8> {
2203        let pattern = seed.to_le_bytes();
2204        let mut out = Vec::with_capacity(len);
2205        while out.len() < len {
2206            out.extend_from_slice(&pattern);
2207        }
2208        out.truncate(len);
2209        out
2210    }
2211
2212    fn oid_of(content: &[u8]) -> String {
2213        hex_of(Sha256::digest(content).as_slice())
2214    }
2215
2216    fn identity() -> Identity {
2217        Identity {
2218            id: "u-writer".to_owned(),
2219            scopes: Vec::new(),
2220            resources: HashMap::new(),
2221        }
2222    }
2223
2224    fn grant_provider() -> Arc<dyn OwnershipProvider> {
2225        let provider = Arc::new(CachedRegistryProvider::new());
2226        provider.install(vec![RegistryRow {
2227            record: RepoRecord {
2228                repo_id: REPO.to_owned(),
2229                visibility: Visibility::Private,
2230                grants: BTreeMap::from([(
2231                    "u-writer".to_owned(),
2232                    vec![AccessAction::Read, AccessAction::Write],
2233                )]),
2234            },
2235            seq: 1,
2236        }]);
2237        Arc::new(LfsGrantProvider::new(provider))
2238    }
2239
2240    #[tokio::test(flavor = "multi_thread")]
2241    async fn the_fleet_handover_retires_every_admission_slot_across_repeated_uploads() {
2242        let dsn = live_pg!();
2243        let schema = unique_schema("lfs-fleet");
2244        let blobs_schema = format!("{schema}_blobs");
2245
2246        let rows = Arc::new(crate::lfs::PgLfsRows::new(
2247            crate::postgres::CoTenantPg::open(&dsn, &schema)
2248                .await
2249                .unwrap(),
2250        ));
2251        let fleet_store = Arc::new(
2252            alkblobs::Store::kv_only_postgres_fleet(
2253                &dsn,
2254                alkblobs::PgOpts {
2255                    schema: blobs_schema.clone(),
2256                    ..alkblobs::PgOpts::default()
2257                },
2258                1024 * 1024,
2259                Duration::ZERO,
2260                Duration::from_secs(60),
2261            )
2262            .await
2263            .unwrap(),
2264        );
2265        let adapter = LfsAdapter::new(
2266            Arc::clone(&fleet_store),
2267            rows.clone(),
2268            grant_provider(),
2269            LfsAdapterConfig {
2270                window_bytes: 128 * 1024,
2271                ..LfsAdapterConfig::default()
2272            },
2273        )
2274        .unwrap();
2275        let identity = identity();
2276
2277        // The fleet arm's precondition: the store's fleet declaration
2278        // resolves the serving instance's id (the mem battery never
2279        // reached this arm — every mint below is a fleet mint through
2280        // `hand_over`'s `Some(fleet)` branch).
2281        let owner = adapter
2282            .ops
2283            .fleet_owner()
2284            .expect("the fleet store carries the serving instance's id");
2285
2286        // Repeated uploads of fresh media: each rides the row
2287        // assertion, the pre-keyed put, then the fleet hand-over —
2288        // `mint_fleet` and the immediate confirm. None refuses at the
2289        // admission cap, and the registry ends empty after every one.
2290        const UPLOADS: usize = 12;
2291        for i in 0..UPLOADS as u64 {
2292            let body = media_of(i, 16 * 1024);
2293            let oid = oid_of(&body);
2294            adapter
2295                .upload(
2296                    Some(&identity),
2297                    REPO,
2298                    &oid,
2299                    Some(body.len() as u64),
2300                    Cursor::new(&body),
2301                )
2302                .await
2303                .expect("the fleet upload lands");
2304            let pairs = rows.repo_rows(REPO).await.unwrap();
2305            assert!(
2306                pairs.iter().any(|row| row.oid == oid),
2307                "upload {i}'s pair asserted its row: {pairs:?}"
2308            );
2309            assert_eq!(
2310                adapter.ops.tokens().len(),
2311                0,
2312                "upload {i}'s mint retired at the confirm — no leaked slot"
2313            );
2314        }
2315        assert_eq!(
2316            rows.repo_rows(REPO).await.unwrap().len(),
2317            UPLOADS,
2318            "one row per upload pair"
2319        );
2320
2321        // The mint boundary's liveness: an explicitly-held token (a
2322        // fleet mint without the confirm — the custody the pin row's
2323        // TTL durably protects) counts its slot, and the next upload's
2324        // own mint lands and retires around it — the admission cap
2325        // stays live per upload with zero leak (the post-upload
2326        // registry carries exactly the held slot, nothing of the
2327        // upload's).
2328        let hold_body = media_of(0xff, 16 * 1024);
2329        let hold_key = lfs_media_key(&oid_of(&hold_body)).expect("the canonical key");
2330        let held = adapter
2331            .ops
2332            .mint_fleet(REPO, hold_key, &owner)
2333            .expect("the boundary mint");
2334        assert_eq!(adapter.ops.tokens().len(), 1, "the held token counts");
2335        let next = media_of(0xfffe, 16 * 1024);
2336        adapter
2337            .upload(
2338                Some(&identity),
2339                REPO,
2340                &oid_of(&next),
2341                Some(next.len() as u64),
2342                Cursor::new(&next),
2343            )
2344            .await
2345            .expect("the upload around the held token lands");
2346        assert_eq!(
2347            adapter.ops.tokens().len(),
2348            1,
2349            "the upload's mint retired around the held one — no leak"
2350        );
2351        assert!(
2352            adapter.ops.tokens().holds(&held),
2353            "the held token's slot persists (its durability is the pin row's TTL)"
2354        );
2355
2356        drop_schema(&dsn, &schema).await;
2357        drop_schema(&dsn, &blobs_schema).await;
2358    }
2359}