Skip to content

feat(nip-fi): harden Blossom kind-24242 verifier to NIP-FI spec - #7288

Open
wpfleger96 wants to merge 36 commits into
mainfrom
duncan/nip-fi-blossom-hardening
Open

wpfleger96 wants to merge 36 commits into
mainfrom
duncan/nip-fi-blossom-hardening

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Sep 3, 2026 •

Copy link
Copy Markdown
Member

Brings buzz-media, buzz-relay, all first-party kind-24242 minters, and desktop token minting into compliance with NIP-FI §kind-24242. Resolves the compliance note added in #7278.

What changed

buzz-media/src/auth.rs

  • Add BlossomStrictness enum (Strict | Permissive). Strict applies full NIP-FI rules (active modes); Permissive preserves pre-NIP-FI Off-mode behavior byte-identical [FI-INV-15].
  • Rewrite verify_blossom_auth_event_for_verb with count-based cardinality: exactly one t/expiration/server in Strict; duplicate x also rejected. Permissive keeps main's boolean semantics exactly: any valued t other than the verb (including "") rejects, valueless t is ignored.
  • Strict: mandatory server tag on all proofs (upload + read); absent or mismatched → evidence_rejected (ServerMismatch).
  • Strict: 60-second proof window (now - created_at <= 60s, expiration <= created_at + 60s). Permissive: 3600s / optional server.
  • [F1] Permissive server validation uses any-match semantics: a proof with multiple server tags is accepted if any tag matches the bound host. This restores origin/main base behavior. Strict duplicate-rejection unchanged.
  • [F1 compat] Permissive server gate uses server_values.is_empty() (valued tags only) rather than server_count > 0 (includes valueless tags). A lone valueless ["server"] tag is treated as absent in Permissive/Off mode, matching pre-NIP-FI base behavior [FI-INV-15].
  • [F4] Valueless x tags in Strict get reads are tracked before filter_map so they cannot become absent scope (host-wide authorization). Any x tag present in Strict must contain the exact requested sha256.
  • [F3 unit] Cases A–D in auth.rs are helper-level unit tests: they call verify_blossom_upload_auth and verify_upload_hash_only directly. They do not enter the upload pipeline (process_upload/process_video_upload). Retained as helper evidence.

buzz-media/src/error.rs

  • Add DuplicateTag(&'static str) variant.
  • Add blossom_denial_kind() method: classifies each auth error as MissingEvidence, EvidenceRejected, or AuthorizationDenied. Non-auth errors return None.
  • IntoResponse retains the legacy JSON 401 shape [FI-INV-15].

buzz-relay/src/api/media.rs

  • Add MediaDenial(MediaError, BlossomStrictness) wrapper mapping to byte-exact NIP-FI fixed responses in Strict; falls through to legacy JSON in Permissive.
  • blossom_strictness_from_state() selects Strict when state.config.nip_fi.is_enforce(), otherwise Permissive. Strictness is threaded through the admit_nip_fi_http_on_state NIP-98 closures on upload and read, and through the post-admission X-SHA-256, hash-binding, and membership denials.
  • extract_blossom_auth keeps first-value Authorization extraction; Enforce rejects repeated fields at the admit_nip_fi_http cardinality gate, and Off keeps legacy behavior [FI-INV-15].
  • Both membership-denial call sites use media_denial(RelayMembershipRequired, strictness) so Strict mode produces the correct fixed-text response.
  • upload_blob serving-write fence conversion (:406) uses map_err(|e| media_denial(e, strictness)) — a community fence committed between auth admission and lease acquisition no longer resets Strict to Permissive.
  • Mode-driven regressions in mod postgres_tests (selected by the nextest postgres-ci profile via test(/postgres_tests::/), #[ignore = "requires Postgres"]), run through the full relay router with a real Enforce verifier and assertion: Enforce rejects Permissive-only proofs (300s lifetime, missing server tag) on both upload routes and reads with evidence rejected; Off accepts the same proofs with pre-NIP-FI responses; DenyProtected answers 503 before any proof check; membership denials are authorization denied text/plain in Enforce and exact legacy JSON 403 in Off, with a member positive control. GET and HEAD are each discriminated (HEAD with a suppressed body). Both upload routes pin the handler's X-SHA-256 missing/malformed/unsigned and post-body hash-mismatch denials in both modes. Off rejects an empty-valued t beside a valid one with legacy 401 JSON in either order, and ignores a valueless one. Exact status, Content-Type, challenge, and body bytes are pinned.

buzz-media/src/upload.rs (mod minio_tests)

  • [F3 pipeline] Four tests exercising the actual post-body call sites (process_upload :85, process_video_upload :413) with real MinIO I/O: buffered/streaming × expired-proof-accept + mismatched-hash-reject. The positive cases use fresh_strict_upload_auth (created_at = now-58, expiration = now+2, lifetime = 60s) — Strict-admissible at sign time — followed by a real 3s sleep so the proof is expired when the upload completes. nostr::Timestamp::now() reads the OS wall clock directly; paused Tokio time does not advance it. The negative cases use a pre-expired proof with a mismatched x tag. All four are #[ignore = "requires MinIO"]; no CI lane runs them automatically.

buzz-media/src/validation.rs

  • minimal_valid_mp4() promoted to #[cfg(test)] pub(crate) — a 618-byte pre-computed H.264/avc1 fast-start MP4 available to all buzz-media test modules; smoke-tested by test_minimal_valid_mp4_passes_validate_video_file.

desktop/src-tauri/src/commands/media.rs

  • sign_blossom_upload_auth: server tag mandatory; upload expiry 60s (was 3600s/300s).
  • MEDIA_GET_AUTH_EXPIRY_SECS: 60s (was 600s).

desktop/src-tauri/src/media_proxy.rs

  • Single re-mint+retry on 401/403 for range requests; non-auth failures do not retry.

crates/buzz-cli/src/client.rs

  • Read expiry: 60s (was 600s). Upload expiry: 60s. server tag mandatory on upload.

crates/buzz-dev-mcp/src/view_image.rs

  • MEDIA_GET_AUTH_EXPIRY_SECS: 60s (was 600s). Test updated.

mobile/lib/shared/relay/media_auth.dart

  • _mediaGetAuthLifetimeSeconds: 60 (was 600). Refresh margin updated.

mobile/lib/shared/relay/media_upload.dart

  • _uploadAuthLifetimeSeconds: 60 (was 300). server tag mandatory.

mobile/lib/features/channels/media_viewer_page/video_viewer.dart

  • [F2] Remove Android-specific VideoPlayerController.networkUrl streaming path. video_player_android 2.9.5 freezes headers into static DefaultHttpDataSource request properties — a 60s proof minted at controller creation time becomes stale on seeks. All platforms use the authenticated local-file download path.
  • [F2 transport] unawaited(request.sink.close()) before client.send(). AbortableStreamedRequest carries no request body; without closing the sink, IOClient.send() awaits stream.pipe(ioRequest) which blocks until the sink ends — every video download hung in loading indefinitely on every platform.
  • [F2 pending-ctrl] pendingController ref set before the first await (initialize()), cleared in all exit paths. Effect cleanup disposes pendingController.value so a close-during-init releases the native player even if the initialized event never arrives.
  • [F2r(a)] On native init failure (post-create, pre-initialized-event), the inner catch starts disposal detached via unawaited(localController.dispose().catchError(...)) — the .catchError handler absorbs any disposal exception so it does not reach the test error zone — then rethrows the original load error. video_player 2.11.1 completes the init future with an error without disposing the native player.
  • [F2r(b)] Non-2xx error body cancelled via _cancelVideoResponse() (subscribe+cancel) instead of drain(). drain() waits for the upstream to close; a stalled server body blocked initializeVideo indefinitely.
  • [F2r(d)] unawaited(localController.dispose().catchError(...)) in the catch — not await. Two failure mechanisms exist: (1) when createWithOptions() throws, _creatingCompleter is never completed and await dispose() deadlocks at :682-683, blocking the outer catch from setting error state. (2) when createWithOptions() succeeds but initialization fails, dispose() runs but its thrown exception becomes an unhandled async error reaching the test zone. Both are fixed: unawaited lets the outer catch run immediately and set error.value; .catchError(...) on the detached disposal future absorbs any disposal exception.

mobile/test/features/channels/media_viewer_page/video_viewer_test.dart

  • _FinalizingFakeClient: calls request.finalize().drain() — a test without the sink fix times out because the drain never completes, validating the transport contract.
  • F2r(a): forceInitError=true fake emits PlatformException on subscribe; disposeCallCount >= 1 verifies disposal even on init failure.
  • F2r(b): stalledBody StreamController with onCancel Completer (bounded completion signal, not a sleep). Asserts: (1) bodyStreamCancelled=true while body stream is still open (impossible with drain()); (2) error UI visible while body remains open. Restoring drain() breaks both.
  • F2r(c): neverInitialize=true fake never sends initialized event; unmount asserts disposeCallCount >= 1 via pendingController. Distinct from the event-error case.
  • F2r(d): forceCreateError=true fake throws PlatformException from createWithOptions() itself (before any player ID is returned). Asserts error UI visible within a bounded deadline. With the old await dispose() the outer catch is blocked — error.value is never set and the error UI assertion fails at the deadline; with unawaited(dispose()) the error text appears immediately.

mobile/test/features/channels/media_viewer_page/video_viewer_transport_test.dart (separate file)

  • Real-IO loopback probes using flutter_test (not package:test) to avoid depend_on_referenced_packages lint. No TestWidgetsFlutterBinding — no HttpClient override interfering with dart:io.
  • Probe 1: HttpServer.bind(loopbackIPv4, 0) reads the full request body before replying. Without sink.close(), req.drain() hangs and the test times out.
  • Probe 2: server signals request arrival via a Completer (not a sleep); abort fires after confirmed arrival; client.send() must throw RequestAbortedException (typed assertion) within a bounded 5s deadline.

scripts/test-video-upload.sh

  • Proof minted with +60 expiry and mandatory server tag.

buzz-test-client/tests/e2e_media*.rs + buzz-relay/src/api/media.rs test fixtures

  • All fixtures updated to 55s expiry + server tag.

docs/nips/NIP-FI.md

  • Compliance note: strict verifier engaged in enforce, Permissive in off, 503 in deny_protected; deny-map (S4) named as the remaining gap.

RELEASING.md

  • New "NIP-FI upload proof freshness (relay floor)" section documenting that 60s proofs require the first relay release carrying this PR's post-body hash-only check (the first tag > v0.2.1 — update once cut) for reliable large-file uploads. Operators on older self-hosted relays (≤ v0.2.1) must upgrade before receiving these clients.

Finding 3: 60s upload proofs vs old-relay compatibility

Desktop/CLI/mobile upload proofs are now 60s. Older relays re-run full expiry verification after the body is transferred. A valid upload whose body transfer takes >60s will fail on an old relay.

Resolution (relay floor prerequisite): RELEASING.md requires that verified supported deployments (relay releases confirmed to carry the post-body hash-only check, commit 75e9bef748) be in place before distributing 60s-proof client builds. Unknown or unverified self-hosted relay deployments are excluded from distribution until they upgrade. This makes the verified-deployment status a prerequisite for release distribution, not a post-distribution advisory. The concrete tag (> v0.2.1) is to be recorded once cut.

Sequencing note

Builds on #7264 (merged): Blossom strictness is derived from its config.nip_fi mode in this PR, so enabling BUZZ_NIP_FI_MODE=enforce activates the strict verifier with no further wiring.

GIF compatibility exception

An unauthenticated Off-mode request on a configured tenant with no GIF provider changes from 404 to 401 (base gifs.rs:269-275 checked an unconfigured provider first; the current code authenticates first). All other Off-mode Blossom responses are byte-identical to pre-NIP-FI behavior.

Brings buzz-media/src/auth.rs, buzz-relay/src/api/media.rs, and the
desktop token minting into full compliance with NIP-FI §kind-24242.

Changes:

buzz-media/src/auth.rs:
- Add BlossomStrictness enum (Strict | Permissive). Strict applies full
  NIP-FI rules; Permissive preserves pre-NIP-FI Off-mode behavior
  byte-identical [FI-INV-15].
- Rewrite verify_blossom_auth_event_for_verb with count-based cardinality
  tracking: exactly one t/expiration/server (Strict), at most one x.
- Strict: mandatory server tag on all proofs (upload + read); absent or
  mismatched -> evidence_rejected (ServerMismatch).
- Strict: 60s proof window (now - created_at <= 60s, expiration <=
  created_at + 60s).
- Permissive: 3600s window, optional server, tolerant cardinality (Off-mode).

buzz-media/src/error.rs:
- Add DuplicateTag(&'static str) variant.
- Split IntoResponse: missing Authorization -> 401 (missing_evidence);
  wrong scheme, malformed, duplicate tags -> 403 (evidence_rejected).

buzz-relay/src/api/media.rs:
- Add blossom_strictness_from_state() helper (TODO: wire to
  config.nip_fi.mode when #7264 lands; defaults to Permissive on main).
- extract_blossom_auth: detect and reject repeated Authorization header
  values -> DuplicateTag("Authorization") -> 403.
- Both call sites (upload + read) now pass strictness to verifier.

desktop/src-tauri/src/commands/media.rs:
- sign_blossom_upload_auth: server tag now mandatory (errors if relay
  URL yields no authority); was conditional.
- Upload token expiry: 60s unconditionally (was 3600s video / 300s image).
- MEDIA_GET_AUTH_EXPIRY_SECS: 60s (was 600s).

desktop/src-tauri/src/media_proxy.rs:
- proxy_handler + handle_buzz_media: single re-mint+retry on 401 or 403
  for range requests (expired 60s token mid-stream).

docs/nips/NIP-FI.md:
- Remove stale compliance note; replace with resolved statement.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
… on #7264

The strict verifier exists but runs in Permissive mode until the NIP-FI
HTTP enforcement PR (#7264) merges and the stub in
blossom_strictness_from_state is replaced with the live mode derivation.
The deny-map gap (S4) is still a named known gap.

Remove the premature 'now compliant' claim and state exactly what is true:
verifier hardening implemented, engagement conditional on #7264 landing,
deny-map pending S4.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Duncan and others added 3 commits September 3, 2026 13:50
…ll minters

Fix two IMPORTANT blockers from Thufir pass 1:

**Fix 1 — mode-aware denial response shape (buzz-media, buzz-relay)**

All strict-verifier failures previously collapsed to a generic JSON 401.
NIP-FI §755-773 requires:
- Missing Authorization → 401 + WWW-Authenticate: Nostr + text/plain
  body 'authentication required\n'
- Malformed/invalid/expired proof → 403 + text/plain 'evidence rejected\n'

The shape is Strict-only — Permissive (Off-mode) keeps the legacy JSON 401
unchanged [FI-INV-15].

Implementation:
- buzz-media/error.rs: add BlossomDenialKind enum (MissingEvidence /
  EvidenceRejected) and blossom_denial_kind() method on MediaError
- buzz-media/lib.rs: export BlossomDenialKind
- buzz-relay/api/media.rs: add MediaDenial(MediaError, BlossomStrictness)
  newtype implementing IntoResponse with mode-aware shaping via DenialClass
  byte contract from buzz-auth. Wire through AuthenticatedUpload extractor
  (Rejection = MediaDenial), authenticate_media_read, get_blob, head_blob.
  Non-auth errors fall through to MediaError::into_response() via From impl.

Tests: response-shape tests for Strict missing-evidence (401 + WWW-Auth +
text body), Strict evidence-rejected (403 + text/plain 'evidence rejected\n',
no WWW-Authenticate), Permissive regression pins for both classes (JSON 401,
no WWW-Authenticate). Classification tests for all 15 error variants.

**Fix 2 — 60s proof window across all first-party minters**

All minters updated to expiration <= created_at + 60s and mandatory upload
server tag, mirroring the desktop pattern established in the prior commit:

- buzz-cli/src/client.rs: read +60 (was +600), upload +60/server mandatory
  (was +600/+3600 conditional server, mime-gated expiry removed)
- buzz-dev-mcp/src/view_image.rs: MEDIA_GET_AUTH_EXPIRY_SECS 60 (was 600),
  comment updated; existing parametric test covers the updated constant
- mobile/lib/shared/relay/media_auth.dart: _mediaGetAuthLifetimeSeconds 60
  (was 600); margin=lifetime → mint-per-request pattern, comment updated
- mobile/lib/shared/relay/media_upload.dart: _uploadAuthLifetimeSeconds 60
  (was 300); server tag mandatory (was conditional on extractServerAuthority)
- scripts/test-video-upload.sh: expiry +60 (was +300), adds server tag
- buzz-relay/src/api/media.rs test fixtures: +55 (was +300) in
  media_get_tags_for and media_read_rejects_upload_verb_wrong_server_and_wrong_x
- buzz-test-client e2e fixtures: +55 + mandatory server tag in all three
  e2e_media* test files (relay_server_authority() helper added)

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The two loop tests moved MediaError into MediaDenial but then
referenced the original variable in format strings. Capture the
debug repr as 'label' before the move.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
get_blob and head_blob are pub fn returning Result<_, MediaDenial>
which exposed the private type in the public interface (E0446).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…r boundaries

Three call sites in the upload path bypassed the MediaDenial strictness
split via From<MediaError> for MediaDenial, which hardcodes Permissive:

1. ok_or(MissingTag("x-sha-256")) — replaced with ok_or_else using
   media_denial(e, strictness).
2. HashMismatch.into() × 2 (malformed + unmatched x tag) — replaced with
   media_denial(HashMismatch, strictness).
3. upload_blob returned Result<_, MediaError>, so post-body failures hit
   MediaError::into_response() directly.

Fix: add strictness: BlossomStrictness to AuthenticatedUpload, derived in
the extractor; change upload_blob to return Result<_, MediaDenial>; apply
media_denial through both the outer (protect-layer) and inner (async body)
error stacks. Non-Blossom errors (I/O, fencing, concurrency) fall through
to the legacy shape in both modes as the wrapper already guarantees.

Add 4 response-shape tests (Strict 403 + Permissive 401 for each of
MissingTag("x-sha-256") and HashMismatch), pinning status, content-type,
body bytes, and WWW-Authenticate absence.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…; fix clippy

Mobile tests and class documentation still specified the superseded
600/300-second proof lifetime after the NIP-FI 60-second fix landed.

media_auth.dart:24-28: rewrite class doc — with lifetime == margin == 60s,
_refreshAt == signedAt and the cache never hits; describe the intentional
mint-per-request pattern instead of the stale memoization claim.

media_image_test.dart:34-58 (memoization group): rewrite to pin mint-per-request
behavior. 'repeated calls return byte-identical headers' (asserting identical())
and 're-signs only at +540s boundary' both contradicted production; replaced with
tests that assert consecutive calls produce distinct headers/Authorization values
including without advancing the clock.

media_upload_test.dart:295: expiration literal 1700000600 (+600s) → 1700000060 (+60s).
media_upload_test.dart:422: expiration literal 1700000300 (+300s) → 1700000060 (+60s).

All 48 affected Dart tests pass on the pinned Flutter 3.41.7 toolchain (confirmed
the pre-fix image_test memoization tests were failing — asserting identical() true
when mint-per-request returns distinct instances every call).

auth.rs:321: x_tags.iter().any(|&v| v == sha256) → x_tags.contains(&sha256)
(clippy::manual_contains — fixes Rust Lint + Windows Rust CI red lanes at 5df4caf).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…r-move in error.rs test

upload_blob, get_blob, and head_blob were pub but only reachable from
router.rs within the same crate — private_interfaces lint fired because
their return types include pub(crate) MediaDenial. Change all three to
pub(crate) to match the crate's visibility posture.

Also fix a borrow-after-move in buzz-media error.rs:
evidence_rejected_errors_permissive_shape_is_json_401 used {error:?}
after error.into_response() moved it. Add let label = format!() before
the move (same pattern already applied in media.rs tests).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…403)

The NIP-FI rejection table (§Public denial classes and transport codes)
maps `evidence_rejected` — malformed, invalid, OR expired evidence — to
HTTP 403.  The previous code mapped signature failures, expired tokens,
missing tags, hash/server mismatches, etc. to 401, matching a now-removed
oracle-enumeration rationale that the spec does not make.

Changes:
- Merge the two `IntoResponse` denial branches into one 403 arm covering
  all structurally present but invalid/malformed/expired proofs.  Only
  absent-header (`MissingAuth`) remains 401.
- Update the body string from "authentication failed" / "authorization
  denied" to the spec's fixed string "evidence rejected" / "authentication
  required" for the respective classes.
- Fix the misleading IntoResponse comment and the now-stale test section
  header.
- Update the `evidence_rejected_errors_permissive_shape_is_json_401` unit
  test → `evidence_rejected_errors_return_json_403`; add `InvalidAuthKind`,
  `InvalidAuthVerb`, `InvalidAuthEvent` to coverage.
- Fix all five `test_auth_*` assertions in e2e_media_extended.rs: wrong
  kind, missing t tag, missing expiration, expired token, empty content all
  expect 403 (not 401).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Duncan and others added 2 commits September 3, 2026 16:07
…ilures

The two CI-failing e2e tests (test_auth_wrong_kind, test_auth_empty_content)
asserted 401 but the server correctly returns 403: InvalidAuthKind and
InvalidAuthEvent are structural format errors that into_response() maps to
403 (observable to any pre-NIP-FI Blossom client — not oracle information).

The previous fix attempt incorrectly merged all evidence_rejected variants
to 403 in into_response(), which broke the Permissive-mode invariant:
MediaDenial (buzz-relay) is the correct layer for the full NIP-FI rejection
table; into_response() is the legacy/Permissive fallback path [FI-INV-15].
Signature, expiry, missing-tag, hash/server-mismatch failures return 401 in
Permissive mode to prevent oracle enumeration — MediaDenial overrides them
to 403 in Strict mode. The media.rs Permissive pin tests document this split.

Changes:
- error.rs: restore two-branch IntoResponse (structural format → 403, oracle-
  guard auth failures → 401). Rewrite comment to document the layering.
  Replace the now-correct-name unit tests: structural_format_errors_return_
  json_403 + evidence_rejected_errors_return_json_401_in_permissive_mode.
- e2e_media_extended.rs: fix only wrong_kind (401→403) and empty_content
  (401→403); revert missing_t_tag, missing_expiration, expired_token to
  401 — they exercise the Permissive path and correctly return 401.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
MediaError::into_response() is the legacy/Permissive compatibility path.
FI-INV-15 requires it to preserve pre-NIP-FI behavior: all auth failures
return a single JSON 401 "authentication failed" response regardless of
failure class.

The 77fabf3 and 151ec74 commits both violated this invariant by
splitting auth failures into 401 and 403 arms inside into_response().
The NIP-FI rejection-table split (missing_evidence → 401,
evidence_rejected → 403) belongs exclusively to MediaDenial in buzz-relay,
which already implements it correctly under BlossomStrictness::Strict.
Routing is currently hardcoded Permissive, so all live traffic and the
Relay E2E lane exercise this legacy path.

Changes:
- error.rs: collapse the two auth arms back to a single 401 arm covering
  all variants (MissingAuth, InvalidAuthScheme, InvalidBase64,
  InvalidAuthEvent, InvalidAuthKind, InvalidAuthVerb, DuplicateTag,
  InvalidSignature, TokenExpired, TimestampOutOfWindow, Unauthorized,
  TokenRevoked, PubkeyMismatch, HashMismatch, ServerMismatch, MissingTag).
  Replace the split unit tests with a single exhaustive
  all_auth_failures_return_json_401_in_permissive_path pin.
- e2e_media_extended.rs: revert all 5 test_auth_* assertions to 401;
  the suite exercises the Permissive path via hardcoded Permissive routing.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Add a body assertion to all_auth_failures_return_json_401_in_permissive_path
so the pin fully captures the FI-INV-15 byte-identical claim: all 16 auth
variants must emit {"error":"authentication failed"} as the JSON body.
The test is promoted to async (#[tokio::test]) to collect the response body
via axum::body::to_bytes; tokio with test-util is already a dev dep.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 deployed to codex-review September 3, 2026 21:02 — with GitHub Actions Active
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026 •

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is 02c6309f534050e54c8d36f5ce90e48e4dd152b1...651ab57d21f55128d9a1758e5058c1d6b3184539.
A new review must complete for this exact range. When manual authorization
is required, a Block organization member must comment exactly
@buzz-security-review 651ab57d21f55128d9a1758e5058c1d6b3184539 to authorize a new review.
Any previous review applies only to its recorded range.

…-body only

Two security findings from Codex review of #7288:

Finding 3 (security): In verify_blossom_auth_event_for_verb, the t tag arm
incremented t_count unconditionally before checking content, so a t tag with
no value (e.g. ["t"]) satisfied the required-tag check (t_count > 0) without
binding any verb. A verb-less proof was accepted for both upload and get in
both Strict and Permissive modes.

Fix: a t tag only counts toward t_count when it has non-empty content equal to
the requested verb. A valueless or empty-string t tag is ignored for cardinality
purposes (matches origin/main's found_t semantics). Duplicate-t cardinality in
Strict mode is checked after confirming the tag is valid, not before.

Also audited expiration/x/server tag handling: these are all gated on
tag.content() already, so valueless variants of those tags are already safe.

Finding 2 (correctness): Both buffered and video upload paths called
verify_blossom_upload_auth post-body, which re-runs the full verifier including
expiry/freshness checks. With 60s minted tokens (correct per NIP-FI Freshness),
any upload taking >60s fails AFTER transferring the full body.

Fix: introduce verify_upload_hash_only — checks only the x-tag hash against the
computed SHA-256. Replace both post-body verify_blossom_upload_auth calls with
this targeted check. The pre-body gate at the relay handler already enforces
signature, kind, freshness, cardinality, and server; the only thing unknown
before body transfer is the content hash.

Tests added: valueless t (Strict+Permissive), empty-string t (Strict), valueless
x on upload. All 147 buzz-media lib tests pass; all 44 api::media + 56
api::admin buzz-relay tests pass.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Sep 3, 2026
In Strict mode, count every tag whose field name is 't' before
validating its content.  The previous implementation counted only
valid-valued matching tags, so a proof with both ["t"] (valueless) and
["t","upload"] (valid) silently ignored the malformed instance and
admitted a two-tag proof as exactly one.

NIP-FI.md:658-666 requires Strict to reject malformed, empty, duplicate,
or conflicting instances as evidence_rejected.  The fix counts first,
gates on cardinality (>1 → DuplicateTag("t")), then validates content
(empty/valueless/wrong-verb → InvalidAuthVerb).  Permissive keeps the
origin/main found_t semantics unchanged.

Updated tests: the two malformed-alone Strict tests now expect
InvalidAuthVerb (counted as one, content check fires) instead of the
stale MissingTag.  Added four new regressions: valueless+valid combo,
empty-string+valid combo (both reject in Strict), and valueless+valid x
combo (confirms x_count increments unconditionally → DuplicateTag("x")).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 deployed to codex-review September 3, 2026 23:16 — with GitHub Actions Active
The two malformed-t-alone Strict tests were named
'is_not_counted_strict' after the previous semantics (ignore →
MissingTag).  At 41ee983 the behavior became count-then-reject →
InvalidAuthVerb, so the names were misleading.  Rename to
test_strict_rejects_valueless_t_tag and
test_strict_rejects_empty_string_t_tag.  Logic unchanged.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carl, an automated reviewer, commenting via Wes’s GitHub account.

Changes requested: the same remaining P2

Reviewed ad6ac0612de3137320028cacb4db24c044a35a20 against exact base 77729abfb692b25a0f4ec4a69add86af2e32c0dd, concentrating on the two-file corrective delta since 08c4152609ad93733fc4c45211c2f1a9d0ce16cb and the exit from review 5279715271. Relay-first rollout remains a valid solution. No client negotiation system or automated deployment gate is requested.

[P2] Require verification of the repaired relay before distributing 60-second-proof clients

Partial fix credited: RELEASING.md:103–116 removes the invented relay-v0.5.0 and unsupported already-deployed assertion. However, the replacement only asks release operators to document a floor and advise self-hosted upgrades. It never requires verifying that the relevant hosted/target relays actually run the repaired implementation before desktop/CLI/mobile distribution. Removing the deployment assertion is not the deployment check requested in the previous review.

The impact is unchanged: these clients issue 60-second upload proofs, while the older video upload pipeline reruns full verification after transfer. Source-derived reproduction, not executed: upload a valid video over 90 seconds to that older implementation. Admission can succeed, then completion fails on proof expiry; re-signing and repeating another 90-second transfer does not fix it. The head pre-body admission and post-body hash-only check remain correctly separated.

Smallest exit: make the release prerequisite explicit, for example:

Before distributing desktop, CLI, or mobile builds that mint 60-second upload proofs, verify that the relevant hosted and target self-hosted relays run a build containing repair commit 75e9bef748d2149ce459b14da842e706a51a5f78 (or a verified containing release tag). Do not distribute to those environments until this check passes. Record the concrete containing tag here when released.

That commit is an ancestor of the reviewed head and contains the post-body repair. A new release tag or assertion that deployment already happened is not required to fix these instructions; carrying out the rollout remains an operational prerequisite. Also change lines 95–98 from “The relay in Strict mode” to “the older full-verifier relay implementation”: head Strict admission does not have this defect.

Coverage and preserved scope

  • Preserve the previously requested regression bound to the production upload caller: admission before proof expiry, body completion after expiry, matching hash succeeds, mismatched hash fails. No media/relay upload source or tests changed in this corrective delta. In the inspected exact-head buzz-media auth/upload tests, relay media-handler tests, and buzz-test-client/tests/e2e_media*.rs, that timing witness is still absent. Existing expired-token coverage rejects at admission, not after an admitted transfer; a helper-only test would not catch the production caller reverting to full verification.
  • The new viewer test exercises actual widget teardown and would catch removal of activeRequestAbort.complete(). Nonblocking witness weakness: _StallingAbortableClient.send sets abortObserved even when the request is not abortable or its trigger is null. Removing that wiring therefore skips the wait and can still satisfy the assertion. Fail explicitly on missing type/trigger and assert no abort before unmount; this is not a new production blocker.
  • Previously credited F1/F2/F4, transport/init cleanup, membership response-byte tests, and Strict serving-write denial handling remain credited. Deferred #7264 activation, assertion pairing, and deny-map integration remain out of scope; permissive expiration semantics and acquisition-fence handler coverage remain nonblocking. This is not a whole-feature PASS.

Validation limits: source-only review on pinned Blox, with independent source/release/metadata lanes and coordinator verification. No checkout, installation, build, tests, PR/dependency execution, native playback, or live slow-transfer probe. One existing exact-head CI snapshot at 2026-09-22 16:06:15 UTC contained 87 contexts: 58 success, 28 skipped, one cancelled (Run Codex Security Review); rollup FAILURE, with no failing test/build context in that snapshot. No reruns or monitoring. Deployed relay state was not verified.

IMPORTANT 1 (new detached-disposal error path): video_viewer.dart already
had the unawaited(dispose().catchError(...)) fix from ad5b0744. This commit
adds _FailingDisposeVideoPlayerPlatform (creates OK, emits init error,
throws from dispose()) and F2r(d)+: verifies error UI appears and no
uncaught Flutter error reaches the test binding's handler.
FlutterError.onError override removed — testWidgets fails automatically
if any uncaught error escapes.

IMPORTANT 2 (abort false-positive): _StallingAbortableClient.send() uses
a typed if-case match (:final abortTrigger?) and throws StateError on
a missing trigger — absent wiring cannot silently pass. Asserts abort NOT
observed before unmount; awaits abortObservedCompleter (bounded 5 s) after
unmount. Added tester.pump() after pumpWidget(SizedBox.shrink()) to flush
the useEffect cleanup microtasks before the Completer deadline.
Red-with-reverted activeRequestAbort.complete(): trigger stays pending,
abortObservedCompleter times out.
Red-with-non-abortable-request: StateError from fake fails fast.

IMPORTANT 3 (F3 relay floor): RELEASING.md — identifies the repair by
commit 75e9bef; adds explicit prerequisite
(verify deployed artifact before distributing 60s-proof clients); corrects
the Strict/old-relay description (old relay re-verifies post-body, head
Strict verifies before body). Four boundary-regression tests added to
auth.rs (Cases A-D: admission-before-expiry, old-verifier-rejects-expired,
hash-only-accepts-expired, hash-mismatch-rejected).

IMPORTANT 4 (Carl follow-up / FI-INV-15): Permissive expiration parsing
now uses last-wins semantics — a valueless expiration tag followed by a
future-valued tag admits, matching pre-NIP-FI base behavior. Test added.

MINORs (all folded):
- F2r(d) mechanism comment corrected: 300ms window ends with error.value
  unset, not 'times out'; no dispose count claimed for create-failure case.
- F2r(d)+ FlutterError.onError override removed (framework already catches).
- Fake dispose() comment: states fake records native disposal and closes
  stream; does not simulate initialization or unblock initialize() future.
- Transport comment: no-drain choice described as responsibility separation
  (not unverified sink.close() root cause); Future.delayed(60s) replaced
  with socket-close-aware serverDone Completer in abort test server handler.
- PR body: F2r(a) 'disposes before rethrowing' -> 'starts disposal';
  viewer-abort description scoped to what it proves; F3 paragraphs state
  prerequisite and regression status accurately.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…est comments

Transport test: add teardown-controlled releaseResponse gate so the
server handler holds the response open until the test releases it.
Without the gate, response.close() sends an empty 200 immediately and
the abort races against a completed response, making the probe
scheduling-sensitive rather than a controlled in-flight cancellation.

Upload pipeline: add streaming-path counterparts to the existing
buffered-path pipeline tests so both call sites (upload.rs:85 and :413)
are bound. The four minio_tests cases now cover:
  - buffered Case B: expired proof + matching hash → accepted
  - buffered Case D: mismatched hash → HashMismatch
  - streaming Case B: expired proof + matching hash → accepted
  - streaming Case D: mismatched hash → HashMismatch

Reverting either verify_upload_hash_only call to the old full verifier
fails the positive (TokenExpired); removing the hash check fails the
negative (security regression). All four marked ignore=requires MinIO.

Add minimal_valid_mp4() at crate scope (#[cfg(test)] pub(crate)) in
validation.rs: 618-byte pre-computed H.264/avc1 fast-start MP4 that
passes validate_video_file, accessible to upload.rs test modules without
exposing the builder internals. Backed by smoke test
test_minimal_valid_mp4_passes_validate_video_file.

Minor test comment fixes: correct 'fails fast' wording for
_AbortingFakeClient and deletion mutation path (test:324-326, 797-798);
rewrite fake-stream dispose comment to state what it actually does
(records disposal, closes stream, does NOT settle initialize() future);
replace 300ms sleep with disposedCompleter.future.timeout(5s) and add
disposeCallCount assertion for the disposal-failure test.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…import

Sort buzz_core::tenant before nostr in minio_tests use block; reformat
multi-line expressions to match rustfmt output; remove the unused
sha2::Digest as _ trait import (sha2::Sha256::digest is called fully
qualified so the trait alias is dead and fails Clippy -D warnings).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Duncan and others added 2 commits September 22, 2026 18:12
…meout zone

upload.rs (minio_tests):
- Replace expired_upload_auth in the positive cases with
  fresh_strict_upload_auth (created_at=now-58, exp=now+2, lifetime=60s).
  The previous 119s-lifetime proof was inadmissible under Strict
  (expiration > created_at+60) so 'simulates previously admitted' was
  false for the stated 60s Strict case.
- Assert verify_blossom_upload_auth(Strict) passes at sign time before
  the sleep so the admission claim is verified, not assumed.
- Add a real 3s sleep in both positive tests: nostr::Timestamp::now()
  reads the OS wall clock directly; paused Tokio time does not advance it.
  After 3s the proof is expired; verify_upload_hash_only still accepts it.
- Keep expired_upload_auth for the hash-mismatch negative cases (no
  admission sequence needed there).
- Update module comment to match the actual T=0/T=3s sequence.

video_viewer_test.dart (F2r(d)+):
- Replace .timeout(const Duration(seconds: 5)) on disposedCompleter.future
  with a synchronous isCompleted check after pumpAndSettle(). Outside
  runAsync the flutter_test binding runs FakeAsync; Duration-based timers
  created there are never advanced automatically, so the 5s timeout would
  hang to the 30s outer runner timeout instead of failing promptly.
  isCompleted is the correct oracle: disposedCompleter is completed
  synchronously at the TOP of dispose() (before any async work), so it
  must be complete by the time pumpAndSettle() drains microtasks and
  returns. Removing the unawaited disposal call leaves the completer
  incomplete and the check fails immediately with a diagnostic.
- Remove unsupported dart:io/pending-timers claim from _SimpleVideoPlayerPlatform
  dispose comment; retain only the accurate description.

video_viewer_transport_test.dart:
- Correct probe-2 file header: the Completer is a teardown-controlled
  release gate, not a client-connection-close notification.
- Remove SocketException promise from server handler and teardown
  comments: close() may throw on abort but the exception type is not
  guaranteed.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…-hardening

* origin/main:
  docs(nip-fi): clarify federated identity amendments (#7803)
  fix(desktop): refresh channels after access-revoked closure (#7784)
  fix(desktop): bound startup request bursts and recover quota refusals (#7790)
  fix(audit): frame hash inputs with TLV (#7492)
  fix(admin): allow cold storage worker DB startup (#7770)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carl, an automated reviewer, commenting via Wes’s GitHub account.

Review clear: prior release blocker resolved

Reviewed 19ea00ba0378af520f0f8738f5ff7ad8ea1c986e against exact base 77729abfb692b25a0f4ec4a69add86af2e32c0dd, concentrating on the seven-file corrective delta since ad6ac0612de3137320028cacb4db24c044a35a20 and the agreed exit from review 5280683315. No remaining blocking finding in this bounded re-review. This is a comment, not an approval or certification of deployment/CI.

Resolved and credited

  • Relay-first compatibility: RELEASING.md:94–129 now requires verifying that supported relay environments run a release containing repair commit 75e9bef748d2149ce459b14da842e706a51a5f78 before desktop/CLI/mobile distribution. Unknown or unverified deployments do not satisfy the prerequisite. The wording correctly attributes the defect to older relays and asks for the concrete containing tag once released, without inventing one or claiming deployment. Repair ancestry is verified. Actual rollout verification remains an operator prerequisite, not something this review established.
  • Upload and legacy semantics: relay admission still verifies freshness before body consumption; buffered and streaming production callers retain hash-only completion checks (upload.rs:85,413). The new pipeline tests reach those real callers and add matching/mismatched-hash controls. Permissive expiration parsing now restores the base implementation’s last-valued-wins behavior without changing Strict duplicate rejection.
  • Mobile corrective delta: detached disposal now handles errors without delaying the original load-error UI (video_viewer.dart:184–216). The strengthened viewer test requires an actual abortable request/trigger and observes no abort before unmount. The separate IOClient transport probe holds the response open until cancellation. Previously accepted verifier, playback, membership-denial and serving-write repairs remain credited.

Nonblocking coverage qualification

The new MinIO pipeline tests are useful production-bound post-body checks, not yet a same-proof admission-before-expiry/completion-after-expiry witness. expired_upload_auth sets created_at = now - 120 and expiration = now - 1: that 119-second lifetime cannot pass Strict admission. The fresh-admission unit test uses a different proof. All four pipeline cases are ignored by default; the inspected _ci-relay.yml ignored-test selectors do not select them. Follow up with a Strict-admissible proof carried across the boundary and an explicit infrastructure-backed invocation. This is a coverage limitation, not evidence that the repaired production path is wrong, and does not reopen the resolved release blocker.

Scope and validation

Independent upload, mobile and release/history lanes returned and were integrated with coordinator source verification. Unchanged minters and previously credited GET/HEAD/denial paths were not independently recertified. Deferred #7264 activation, assertion pairing and deny-map integration remain excluded; this is not a whole-feature PASS.

Source-only on pinned Blox: no checkout, install, build, tests, PR/dependency execution, native playback or live slow-transfer/deployment probe. Static PNG chunk/CRC and MP4 box-bound inspection is not a runtime validator pass. One existing exact-head CI snapshot contained 87 contexts: 54 success, 28 skipped, four failures (Desktop Smoke E2E shards 1 and 4 plus two Desktop aggregates), and one cancelled Codex Security Review. No cause inferred, reruns or monitoring; CI is not green.

@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Sep 22, 2026
The previous isCompleted check after pumpAndSettle() was racy: the
viewer does real File.openWrite() before player creation, and with
disableAnimations:true the loading widget's reduced-motion branch stops
animation, so pumpAndSettle() (a frame barrier, not an I/O barrier)
can return while I/O is still pending. isCompleted then fails on
unchanged production code, misdiagnosing a removed disposal path.

Fix (Shape A): construct _FailingDisposeVideoPlayerPlatform and mount
the widget inside runAsync. Await disposedCompleter.future with a
real-zone 5s timeout, also inside runAsync. Timers registered inside
runAsync dispatch to the real event loop — they fire normally. The
completion microtask runs in the same real zone (initializeVideo()
starts there, the inner catch fires there, the unawaited detached
disposal future runs there, and dispose() completes the completer
synchronously before throwing). The await therefore resolves as soon
as the microtask queue drains after dispose() entry — no I/O race.

Removal check: removing only the unawaited disposal call leaves
disposedCompleter never completed; the 5s real-zone timeout fires
and the test fails immediately and deterministically.

Also update the _FailingDisposeVideoPlayerPlatform.disposedCompleter
docstring to name the zone mechanism.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Sep 22, 2026

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carl, an automated reviewer, commenting via Wes’s GitHub account.

Review clear: bounded corrective re-review

Reviewed HEAD 4afefe42639907f15dbb124ce7b0ebf96088b921 against BASE 26ede6dfa2496993aa62ce5781d2112df4c2d009, continuing the prior clear review. No actionable blocker found in the corrective delta. This is a comment, not an approval or whole-feature certification.

  • Scope: after subtracting inherited base movement, the branch-specific changes are confined to crates/buzz-media/src/upload.rs tests and the two mobile video-viewer test files. Previously credited production fixes, relay-first release policy, and the NIP-FI branch amendment remain unchanged. Supported relays still must be verified to contain repair 75e9bef748d2149ce459b14da842e706a51a5f78 before distributing 60-second-proof clients; this review does not establish deployment.
  • Upload witness: buffered and streaming positives now pass the same Strict-admissible proof through explicit verification, a real three-second wait, and the production completion pipeline. This addresses the prior same-proof coverage note. It is still a verifier-to-pipeline test, not a real HTTP slow-transfer test; the four MinIO cases remain ignored by default, and this review did not execute them. The short admission margin and wall-clock dependency remain test reliability limitations, not evidence of a production regression.
  • Mobile witness: F2r(d)+ now waits for disposal entry with a real-zone five-second timeout inside runAsync, instead of a fixed delay followed by a FakeAsync-zone wait. It still requires the error UI and an observed disposal call from a fake that throws during disposal. The transport-file delta only corrects comments about the held response and possible socket-close exception.

Validation and limits

Independent upload and mobile source-review lanes were integrated with coordinator checks on the pinned Blox host. No checkout, installation, build, test, PR/dependency execution, native playback, live slow-transfer probe, CI rerun or monitoring was performed. Existing exact-head check snapshot: 59 successful, 28 skipped, one cancelled security-review job, no failed checks; the Mobile job succeeded. This does not establish an executed MinIO witness.

Unchanged client minters and earlier GET/HEAD/denial repairs were not independently recertified. Deferred #7264 activation, assertion pairing, and deny-map integration remain excluded. No new merge condition is introduced by this re-review.

@bradseiler

bradseiler commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Integration TODO before merge:

…strictness

Blossom strictness now follows NIP-FI mode: Enforce selects the Strict
kind-24242 verifier, Off keeps Permissive. Folds the strictness plumbing
into main's UploadContext + admission-closure structure, drops the
test-only strictness override, and restores first-value Authorization
extraction (Enforce cardinality is owned by the admission gate).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…denials

Off mode accepted an empty-valued t tag beside a valid one, which main
rejects with legacy 401 JSON (FI-INV-15). Route tests now also pin HEAD
strictness and the handler's X-SHA-256/post-body hash denials in both modes.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Pins HEAD's membership-call strictness, which GET-only cases could not discriminate.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@github-actions github-actions Bot added codex-security-review-current The posted Codex security review matches its recorded range. and removed codex-security-review-current The posted Codex security review matches its recorded range. labels Sep 25, 2026
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carl, an automated reviewer, commenting via Wes’s GitHub account.

Review clear for this delta; no blocking findings. Reviewed head 651ab57d21f55128d9a1758e5058c1d6b3184539 against base 6410e685a80d42db0645fadc9bbe710559ef911e, following the previous clear review.

  • Verified the new integration with #7264: Enforce selects Strict consistently for upload, GET and HEAD; assertion-key pairing remains intact; post-admission hash, membership and write-fence denials retain mode-specific responses. Off restores main’s empty-valued/valueless t behavior. DenyProtected rejects before proof verification.
  • Source-only review on pinned Blox, with independent auth/privacy and contract lanes. Existing exact-head Unit, PostgreSQL and Relay E2E checks passed. Run Codex Security Review was cancelled, not a security-review pass. No PR code was executed during this review; the previously documented MinIO expiry-test coverage limitation remains.
  • The relay-first distribution prerequisite remains: verify every supported relay contains repair 75e9bef748d2149ce459b14da842e706a51a5f78 before shipping 60-second-proof clients. Deployment verification is not established here. The inherited S4 deny-map gap and unchanged, previously reviewed client paths are not newly certified.

This is a bounded re-review, not whole-feature or rollout approval.

This branch was successfully deployed

No deployments
codex-review — 651ab57d Deployed Sep 25, 2026 by wpfleger96 via Run Codex Security Review #5816
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants