feat(node): iCaptcha proof-of-intelligence gate on create_repo + register#108
feat(node): iCaptcha proof-of-intelligence gate on create_repo + register#108kevincodex1 wants to merge 6 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds an ChangesiCaptcha Proof Gate
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
2e258ee to
4f39baf
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/gitlawb-node/src/icaptcha.rs`:
- Around line 51-56: The parse_mode helper in icaptcha.rs currently maps any
unrecognized ICAPTCHA_MODE value to Mode::Off, which silently disables
enforcement. Update parse_mode (and the other call site referenced by the
comment) so invalid inputs are detected and surfaced with a warning or error log
before falling back, using the existing Mode enum and the icaptcha configuration
path to keep operator-visible signal for typos like “enforced”.
- Around line 98-101: The startup key fetch in fetch_key is currently using
reqwest::get without any timeout, so a hung /v1/pubkey endpoint can block
icaptcha::init during startup. Update fetch_key to use a reqwest client or
request builder with a timeout for the pubkey request, and keep the existing
fallback behavior so init can return inert when the fetch does not complete.
Make the change in the fetch_key path used by icaptcha::init and main startup,
preserving the current JWKS parsing and decode_key flow.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8d94b947-513c-42cd-ad24-cecbc38170e7
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (5)
crates/gitlawb-node/Cargo.tomlcrates/gitlawb-node/src/api/register.rscrates/gitlawb-node/src/api/repos.rscrates/gitlawb-node/src/icaptcha.rscrates/gitlawb-node/src/main.rs
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.env.example:
- Around line 98-105: Reorder the iCaptcha environment entries in the example so
dotenv-linter no longer flags them: move ICAPTCHA_PUBKEY and
ICAPTCHA_REQUIRED_LEVEL above ICAPTCHA_URL while keeping the existing
ICAPTCHA_MODE and related comments intact. Update the .env.example block in
place so the key ordering is consistent with the linter’s expected
alphabetical-style grouping for the ICAPTCHA_* variables.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3afe1e80-c2ef-4e52-8b1e-45e48ece29f3
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (6)
.env.examplecrates/gitlawb-node/Cargo.tomlcrates/gitlawb-node/src/api/register.rscrates/gitlawb-node/src/api/repos.rscrates/gitlawb-node/src/icaptcha.rscrates/gitlawb-node/src/main.rs
🚧 Files skipped from review as they are similar to previous changes (5)
- crates/gitlawb-node/Cargo.toml
- crates/gitlawb-node/src/api/repos.rs
- crates/gitlawb-node/src/main.rs
- crates/gitlawb-node/src/api/register.rs
- crates/gitlawb-node/src/icaptcha.rs
jatmn
left a comment
There was a problem hiding this comment.
Findings
-
[P2] Complete CodeRabbit's request to surface invalid iCaptcha modes
crates/gitlawb-node/src/icaptcha.rs:51
CodeRabbit's inline request is still valid:parse_modemaps every unrecognizedICAPTCHA_MODEvalue toMode::Off, so an operator typo such asICAPTCHA_MODE=enforcedsilently disables the protection they were trying to enable. Please complete that review request by accepting only the documented values and logging or otherwise surfacing invalid input before falling back. -
[P2] Complete CodeRabbit's request to bound startup key fetches
crates/gitlawb-node/src/icaptcha.rs:98
CodeRabbit's startup-availability request is still valid:mainawaitsicaptcha::init(), andfetch_keyusesreqwest::get()without a timeout. WithICAPTCHA_MODE=shadoworenforce, a hung/v1/pubkeyendpoint can leave the node stuck during startup instead of reaching the intended fail-safe inert path. Please complete that review request by adding a bounded request timeout while preserving the current fallback behavior. -
[P2] Surface invalid required-level configuration instead of lowering to the default
crates/gitlawb-node/src/icaptcha.rs:110
ICAPTCHA_REQUIRED_LEVELis parsed with.and_then(|v| v.parse().ok()).unwrap_or(3), so an operator trying to raise the gate with a typo such asICAPTCHA_REQUIRED_LEVEL=5orICAPTCHA_REQUIRED_LEVEL=fivesilently runs at level3. Inenforcemode that weakens the configured challenge threshold with no warning. Please validate and warn or reject invalid level values instead of defaulting silently. -
[P3] Complete CodeRabbit's dotenv-linter request for the new env block
.env.example:101
CodeRabbit's later lint comment is still current: the new block leavesICAPTCHA_URLbeforeICAPTCHA_PUBKEYandICAPTCHA_REQUIRED_LEVEL, and its dotenv-linter output flags both asUnorderedKey. Please complete that review request by reordering the newICAPTCHA_*entries so the example stops carrying the lint warning.
…y fetch) Addresses CodeRabbit and @jatmn review feedback on #108: - parse_mode now returns Option and rejects unrecognized ICAPTCHA_MODE values; init() warns and falls back to off instead of silently disabling the gate on a typo like "enforced". [P2] - ICAPTCHA_REQUIRED_LEVEL: warn on a non-empty unparseable value instead of silently lowering the threshold to the default. [P2] - fetch_key uses a reqwest client with a 5s timeout so a hung /v1/pubkey can never block node startup; still fails inert on timeout/error. [P2] - .env.example: reorder ICAPTCHA_* keys (MODE, PUBKEY, REQUIRED_LEVEL, URL) to satisfy dotenv-linter. [P3] - add parse_mode unit test (rejects junk, accepts documented values). cargo test -p gitlawb-node icaptcha: 12 passed. clippy + fmt clean. Author: Kevin Codex <kevin@gitlawb.com>
jatmn
left a comment
There was a problem hiding this comment.
Thanks for the update. I rechecked the changed paths and found one issue that still needs to be addressed.
Findings
- [P2] Consume proof IDs so one solved challenge cannot be replayed
crates/gitlawb-node/src/icaptcha.rs:230
The verifier decodes the signed claims and checksexp,level, andsub, but it does not read or store the proof'sjti/nonce, even though the captured proof fixture includes one. That means the sameX-ICaptcha-Proofcan be reused repeatedly by the same authenticated DID until it expires, including across the spam-pronecreate_repoandregisterroutes this PR is trying to gate. With the existing per-DID throttle, one solved challenge can still cover every allowed creation request in the window, and new-DID registration remains a replayable bearer proof for that DID. Please consume a unique proof ID, at least until itsexp, and reject repeats soenforceactually requires a fresh earned proof for each gated creation action.
Addresses @jatmn P2 on #108: the verifier checked exp/level/sub but never consumed the proof's jti, so one solved challenge was a reusable bearer proof for that DID until expiry, covering every gated create_repo/register in the window. Now, in enforce mode, each verified proof's jti is recorded once and replays are rejected. - db: migration v7 adds icaptcha_consumed_proofs(jti PK, expires_at); new consume_proof_jti (INSERT ON CONFLICT DO NOTHING -> bool) and sweep_expired_proofs. DB-backed so it holds across restarts and instances. - icaptcha: ProofClaims carries jti; verify returns the claims; decide() stays pure and returns Allow | Reject | Consume{jti,exp}; check() is now async and spends the jti via the DB, rejecting replays (enforce only; shadow observes without consuming). - handlers: create_repo/register await check(&state.db, ...). - main: periodic sweep of expired proof rows alongside rate-limit cleanup. - tests: 12 pass incl. decide() yields Consume with the proof's jti. Author: Kevin Codex <kevin@gitlawb.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/gitlawb-node/src/icaptcha.rs`:
- Around line 184-205: The `check` flow in `icaptcha.rs` consumes the proof
`jti` too early, before the request is fully known to be admissible. Update
`check`/`Decision::Consume` handling so `register` and `create_repo` can verify
the proof first and only call the consumption path after DID/name/existence
validation has passed, or split verification from consumption entirely. Use the
existing `check`, `decide`, `Decision::Consume`, and `db.consume_proof_jti`
symbols to keep the replay protection but avoid burning valid proofs on rejected
requests.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2fc420e5-6ba2-43cc-8169-8ccd147985d6
📒 Files selected for processing (5)
crates/gitlawb-node/src/api/register.rscrates/gitlawb-node/src/api/repos.rscrates/gitlawb-node/src/db/mod.rscrates/gitlawb-node/src/icaptcha.rscrates/gitlawb-node/src/main.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- crates/gitlawb-node/src/api/register.rs
- crates/gitlawb-node/src/api/repos.rs
…ster Add an optional gate that requires callers to present an iCaptcha proof (an Ed25519-signed token from icaptcha.gitlawb.com, earned by solving an escalating challenge) on the two spam-prone endpoints. Proofs are verified OFFLINE with ed25519-dalek against the published public key, and bound to the authenticated agent DID so they cannot be shared. Backward-compatible by default: ICAPTCHA_MODE is `off` unless set, so deploying changes nothing. `shadow` verifies and logs would-be rejections but always allows; `enforce` rejects requests lacking a valid, strong-enough proof. If the public key cannot be loaded at startup the gate stays inert (fail safe) so an iCaptcha outage can never break repo creation or registration. - New src/icaptcha.rs: env-driven verifier (OnceLock), /v1/pubkey fetch at startup or ICAPTCHA_PUBKEY override, proof parse + signature + expiry + level + DID-binding checks; mode-aware decide() split out for testing. - Wired into create_repo and register (X-ICaptcha-Proof header). git push (receive-pack) is intentionally NOT gated. - 11 unit tests incl. verification of a real captured proof (JS-signed, Rust-verified), tamper/expiry/wrong-DID/level/missing-header rejections, and the off/shadow/enforce/fail-safe mode matrix. Author: Kevin Codex <kevin@gitlawb.com>
…y fetch) Addresses CodeRabbit and @jatmn review feedback on #108: - parse_mode now returns Option and rejects unrecognized ICAPTCHA_MODE values; init() warns and falls back to off instead of silently disabling the gate on a typo like "enforced". [P2] - ICAPTCHA_REQUIRED_LEVEL: warn on a non-empty unparseable value instead of silently lowering the threshold to the default. [P2] - fetch_key uses a reqwest client with a 5s timeout so a hung /v1/pubkey can never block node startup; still fails inert on timeout/error. [P2] - .env.example: reorder ICAPTCHA_* keys (MODE, PUBKEY, REQUIRED_LEVEL, URL) to satisfy dotenv-linter. [P3] - add parse_mode unit test (rejects junk, accepts documented values). cargo test -p gitlawb-node icaptcha: 12 passed. clippy + fmt clean. Author: Kevin Codex <kevin@gitlawb.com>
Addresses @jatmn P2 on #108: the verifier checked exp/level/sub but never consumed the proof's jti, so one solved challenge was a reusable bearer proof for that DID until expiry, covering every gated create_repo/register in the window. Now, in enforce mode, each verified proof's jti is recorded once and replays are rejected. - db: migration v7 adds icaptcha_consumed_proofs(jti PK, expires_at); new consume_proof_jti (INSERT ON CONFLICT DO NOTHING -> bool) and sweep_expired_proofs. DB-backed so it holds across restarts and instances. - icaptcha: ProofClaims carries jti; verify returns the claims; decide() stays pure and returns Allow | Reject | Consume{jti,exp}; check() is now async and spends the jti via the DB, rejecting replays (enforce only; shadow observes without consuming). - handlers: create_repo/register await check(&state.db, ...). - main: periodic sweep of expired proof rows alongside rate-limit cleanup. - tests: 12 pass incl. decide() yields Consume with the proof's jti. Author: Kevin Codex <kevin@gitlawb.com>
8cfa65a to
e976398
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/gitlawb-node/src/api/repos.rs (1)
145-165: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winMove one-shot proof consumption after cheap request validation.
icaptcha::checkrecords the JTI before repo-name validation and the “already exists” check. In enforce mode, a typo or duplicate repo name burns a valid proof, so the corrected retry can fail as a replay even though no repo was created.Proposed fix
- // iCaptcha proof-of-intelligence gate (inert unless ICAPTCHA_MODE is set). - crate::icaptcha::check(&state.db, &headers, &auth.0).await?; - // Sanitize name: alphanumeric, hyphens, underscores only if !req .name .chars() @@ if state.db.get_repo(&owner_did, &req.name).await?.is_some() { return Err(AppError::RepoExists(req.name)); } + + // iCaptcha proof-of-intelligence gate (inert unless ICAPTCHA_MODE is set). + crate::icaptcha::check(&state.db, &headers, &owner_did).await?;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/gitlawb-node/src/api/repos.rs` around lines 145 - 165, Move the iCaptcha proof consumption later in the repo creation flow so cheap request validation happens first. In the create-repo path in `repos.rs`, keep the repo name sanitization and `state.db.get_repo` existence check before calling `crate::icaptcha::check`, then only consume the JTI once the request is known to be valid and non-duplicate. Use the `icaptcha::check` call, `owner_did`, and `state.db.get_repo` as the key anchors when reordering the logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/gitlawb-node/src/api/repos.rs`:
- Around line 881-882: The webhook payload is hard-coding the ref update
metadata as not forced, which misreports force-pushes to downstream consumers.
Update the payload construction in the repo webhook handling logic around the
ref update object so the forced flag is computed from the old/new SHAs instead
of always set to false. Use the existing update context in repos.rs to determine
whether the change is a non-fast-forward update (for example, checking that both
SHAs are non-zero and the old commit is not an ancestor of the new one) before
setting the forced field.
In `@crates/gitlawb-node/src/db/mod.rs`:
- Around line 785-795: The icaptcha replay ledger is keyed only by jti, so a
proof consumed in one gated action blocks reuse in another even though the
contract is per action. Update the icaptcha_consumed_proofs schema and the
related APIs in crate::icaptcha::check to include an action discriminator (for
example register vs create_repo) and enforce uniqueness on the composite key,
then adjust the insert/lookup logic to scope replay checks by both action and
jti.
---
Outside diff comments:
In `@crates/gitlawb-node/src/api/repos.rs`:
- Around line 145-165: Move the iCaptcha proof consumption later in the repo
creation flow so cheap request validation happens first. In the create-repo path
in `repos.rs`, keep the repo name sanitization and `state.db.get_repo` existence
check before calling `crate::icaptcha::check`, then only consume the JTI once
the request is known to be valid and non-duplicate. Use the `icaptcha::check`
call, `owner_did`, and `state.db.get_repo` as the key anchors when reordering
the logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 76257d7a-d8b1-441c-98a8-f2602b38dad6
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (7)
.env.examplecrates/gitlawb-node/Cargo.tomlcrates/gitlawb-node/src/api/register.rscrates/gitlawb-node/src/api/repos.rscrates/gitlawb-node/src/db/mod.rscrates/gitlawb-node/src/icaptcha.rscrates/gitlawb-node/src/main.rs
🚧 Files skipped from review as they are similar to previous changes (5)
- crates/gitlawb-node/Cargo.toml
- .env.example
- crates/gitlawb-node/src/main.rs
- crates/gitlawb-node/src/api/register.rs
- crates/gitlawb-node/src/icaptcha.rs
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/gitlawb-node/src/api/repos.rs (1)
145-165: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winMove one-shot proof consumption after cheap request validation.
icaptcha::checkrecords the JTI before repo-name validation and the “already exists” check. In enforce mode, a typo or duplicate repo name burns a valid proof, so the corrected retry can fail as a replay even though no repo was created.Proposed fix
- // iCaptcha proof-of-intelligence gate (inert unless ICAPTCHA_MODE is set). - crate::icaptcha::check(&state.db, &headers, &auth.0).await?; - // Sanitize name: alphanumeric, hyphens, underscores only if !req .name .chars() @@ if state.db.get_repo(&owner_did, &req.name).await?.is_some() { return Err(AppError::RepoExists(req.name)); } + + // iCaptcha proof-of-intelligence gate (inert unless ICAPTCHA_MODE is set). + crate::icaptcha::check(&state.db, &headers, &owner_did).await?;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/gitlawb-node/src/api/repos.rs` around lines 145 - 165, Move the iCaptcha proof consumption later in the repo creation flow so cheap request validation happens first. In the create-repo path in `repos.rs`, keep the repo name sanitization and `state.db.get_repo` existence check before calling `crate::icaptcha::check`, then only consume the JTI once the request is known to be valid and non-duplicate. Use the `icaptcha::check` call, `owner_did`, and `state.db.get_repo` as the key anchors when reordering the logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/gitlawb-node/src/api/repos.rs`:
- Around line 881-882: The webhook payload is hard-coding the ref update
metadata as not forced, which misreports force-pushes to downstream consumers.
Update the payload construction in the repo webhook handling logic around the
ref update object so the forced flag is computed from the old/new SHAs instead
of always set to false. Use the existing update context in repos.rs to determine
whether the change is a non-fast-forward update (for example, checking that both
SHAs are non-zero and the old commit is not an ancestor of the new one) before
setting the forced field.
In `@crates/gitlawb-node/src/db/mod.rs`:
- Around line 785-795: The icaptcha replay ledger is keyed only by jti, so a
proof consumed in one gated action blocks reuse in another even though the
contract is per action. Update the icaptcha_consumed_proofs schema and the
related APIs in crate::icaptcha::check to include an action discriminator (for
example register vs create_repo) and enforce uniqueness on the composite key,
then adjust the insert/lookup logic to scope replay checks by both action and
jti.
---
Outside diff comments:
In `@crates/gitlawb-node/src/api/repos.rs`:
- Around line 145-165: Move the iCaptcha proof consumption later in the repo
creation flow so cheap request validation happens first. In the create-repo path
in `repos.rs`, keep the repo name sanitization and `state.db.get_repo` existence
check before calling `crate::icaptcha::check`, then only consume the JTI once
the request is known to be valid and non-duplicate. Use the `icaptcha::check`
call, `owner_did`, and `state.db.get_repo` as the key anchors when reordering
the logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 76257d7a-d8b1-441c-98a8-f2602b38dad6
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (7)
.env.examplecrates/gitlawb-node/Cargo.tomlcrates/gitlawb-node/src/api/register.rscrates/gitlawb-node/src/api/repos.rscrates/gitlawb-node/src/db/mod.rscrates/gitlawb-node/src/icaptcha.rscrates/gitlawb-node/src/main.rs
🚧 Files skipped from review as they are similar to previous changes (5)
- crates/gitlawb-node/Cargo.toml
- .env.example
- crates/gitlawb-node/src/main.rs
- crates/gitlawb-node/src/api/register.rs
- crates/gitlawb-node/src/icaptcha.rs
🛑 Comments failed to post (2)
crates/gitlawb-node/src/api/repos.rs (1)
881-882: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Don’t hard-code force-push metadata.
Webhook payloads always emit
"forced": false, so force pushes are misreported to downstream integrations. Compute this per ref, e.g.old != ZERO_SHA && new != ZERO_SHA && old is not an ancestor of new.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/gitlawb-node/src/api/repos.rs` around lines 881 - 882, The webhook payload is hard-coding the ref update metadata as not forced, which misreports force-pushes to downstream consumers. Update the payload construction in the repo webhook handling logic around the ref update object so the forced flag is computed from the old/new SHAs instead of always set to false. Use the existing update context in repos.rs to determine whether the change is a non-fast-forward update (for example, checking that both SHAs are non-zero and the old commit is not an ancestor of the new one) before setting the forced field.crates/gitlawb-node/src/db/mod.rs (1)
785-795: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Scope replay tracking per gated action, not globally per
jti.
crate::icaptcha::checkconsumes proofs from both/registerand/create_repo, but this ledger makesjtiglobally unique. After a successfulregister, the same still-valid proof will be rejected oncreate_repo, which breaks the "spent once per gated action" contract described here. Add an action discriminator to the table/API and make the uniqueness check composite (for example(action, jti)).Also applies to: 1115-1129
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/gitlawb-node/src/db/mod.rs` around lines 785 - 795, The icaptcha replay ledger is keyed only by jti, so a proof consumed in one gated action blocks reuse in another even though the contract is per action. Update the icaptcha_consumed_proofs schema and the related APIs in crate::icaptcha::check to include an action discriminator (for example register vs create_repo) and enforce uniqueness on the composite key, then adjust the insert/lookup logic to scope replay checks by both action and jti.
|
@coderabbitai can you do a full review on this ? |
|
✅ Action performedFull review finished. |
Addresses CodeRabbit (Major) on #108: check() spent the jti as soon as the proof verified, but create_repo/register ran it before name/DID/existence checks, so a rejected request (bad name, already-exists, wrong DID) permanently burned a valid proof without the action succeeding. Split verification from consumption: - icaptcha: replace check() with verify_request() -> ProofGuard (verifies and rejects invalid/missing proofs early; off/shadow/inert yield a no-op guard) plus ProofGuard::consume(db) which spends the jti right before the write and rejects replays. Guard is #[must_use]. - create_repo/register: verify_request() up front, then consume() immediately before the first write, after cheap validation passes. Replay protection unchanged (consume-before-write). 12 tests pass; clippy clean. Author: Kevin Codex <kevin@gitlawb.com>
|
hey @coderabbitai please do a full review again and approve if its all good |
|
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/gitlawb-node/src/icaptcha.rs`:
- Around line 200-205: The iCaptcha failure paths in consume and the other
matching branch are still returning AppError::Unauthorized, which gets mapped to
the not_an_agent code. Update the iCaptcha flow in icaptcha::consume to use a
dedicated AppError variant for missing/replayed proofs, and add the
corresponding mapping in error.rs so these cases return the new
iCaptcha-specific API code instead of the authn one.
🪄 Autofix (Beta)
❌ Autofix failed (check again to retry)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 65edad93-b5f5-43a3-80c0-f16c17fb97f9
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (7)
.env.examplecrates/gitlawb-node/Cargo.tomlcrates/gitlawb-node/src/api/register.rscrates/gitlawb-node/src/api/repos.rscrates/gitlawb-node/src/db/mod.rscrates/gitlawb-node/src/icaptcha.rscrates/gitlawb-node/src/main.rs
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. The branch was updated while autofix was in progress. Please try again. |
Addresses CodeRabbit on #108: iCaptcha failure paths returned AppError::Unauthorized, which maps to the "not_an_agent" API code and conflates a valid-but-unproven agent with a failed signature. Add a dedicated AppError::IcaptchaProofRequired variant mapped to the "icaptcha_proof_required" code (HTTP 401), and use it for both the missing/invalid-proof and replay paths so clients can distinguish "solve a challenge" from "your signature is invalid". 12 tests pass; clippy + fmt clean. Author: Kevin Codex <kevin@gitlawb.com>
|
@coderabbitai please make a full review again . approve if all good |
|
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/gitlawb-node/src/icaptcha.rs`:
- Around line 221-224: Shadow mode is dropping the verified proof ID, so replay
detection never runs in the “would reject” path. Update the flow around decide,
ProofGuard, and the shadow-mode handling in the verification path so a
successful shadow verification still carries the jti forward instead of
collapsing to ProofGuard(None). Then make register and create_repo route that
proof through a read-only replay check/log path, preserving consume behavior
only for enforce mode and leaving shadow mode to report reused jtis without
mutating state.
🪄 Autofix (Beta)
✅ Autofix completed
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4435a3b7-fcb4-4611-a4e5-33514e1824d3
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (8)
.env.examplecrates/gitlawb-node/Cargo.tomlcrates/gitlawb-node/src/api/register.rscrates/gitlawb-node/src/api/repos.rscrates/gitlawb-node/src/db/mod.rscrates/gitlawb-node/src/error.rscrates/gitlawb-node/src/icaptcha.rscrates/gitlawb-node/src/main.rs
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 2 file(s) based on 1 unresolved review comment. Files modified:
Commit: The changes have been pushed to the Time taken: |
Fixed 2 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
beardthelion
left a comment
There was a problem hiding this comment.
Reviewed the merged state at 66e7a34 (CI green, merge clean). The core gate is sound and I confirmed the load-bearing parts by execution: Ed25519 verify_strict, the proof-to-DID binding via the shared did_matches normalizer (handles short/full did:key, not a bare ==), atomic single-use through INSERT ... ON CONFLICT DO NOTHING, the verify-then-consume split, and that the gate is additive to the existing owner/auth checks (INV-1 intact). Migration 8 is the correct next version, additive and idempotent. The CodeRabbit threads and the two earlier change requests are all addressed on this head, including the shadow read-only replay path.
Two things to close before merge, then a few notes.
Findings
-
[P2] Gate
fork_repo, or state why it is exempt
crates/gitlawb-node/src/api/repos.rs:1420
fork_repocreates a repo (db.create_repoafter agit clone --mirror) and never callsicaptcha::verify_request; its handler signature does not even takeHeaderMap. Underenforcean agent bypasses thecreate_repo/registergate entirely by forking. It is the third repo-creation entrypoint and the one that previously drifted on visibility (#98).create_repoandregisterare gated and GraphQL exposes no creation mutation, so fork is the only open path. Either thread the gate through it (verify before the clone, consume just beforedb.create_repo) or document the exemption with the reasoning. -
[P2] Add DB-level tests for the single-use ledger
crates/gitlawb-node/src/db/mod.rs:1119
consume_proof_jti,check_proof_jti,sweep_expired_proofs, andProofGuard::consumehave no execution coverage, although a#[sqlx::test]harness already exists in this same file. Single use is the feature's security value, and right now nothing runs it. The path itself is correct (I checked: first consume succeeds, replay returns false,check_proof_jtidoes not mutate, sweep keeps still-valid rows and drops expired), so this is just landing those as committed tests.
Notes
-
[P3] Document
ICAPTCHA_PUBKEYas the supportedenforceconfiguration
crates/gitlawb-node/src/icaptcha.rs:246
decide()returnsAllowwhen no key loaded, so an unreachable/v1/pubkeyat startup degradesenforceto inert with only a warn. That is a reasonable availability choice, but it meansenforcefails open on a startup network blip. PinningICAPTCHA_PUBKEYremoves the startup fetch dependency; worth calling it out as the way to runenforce. -
[P3] Consider 403 rather than 401 for
IcaptchaProofRequired
crates/gitlawb-node/src/error.rs:60
The caller is already HTTP-signature authenticated; what is missing is an anti-abuse proof, which is closer to 403 Forbidden than 401. The distincticaptcha_proof_requiredcode is right; only the status is debatable. Confirm no client keys on the numeric 401 before changing it. -
[P3] Confirm the challenge has a headless solve path before flipping
enforce
The verification side is agent-clean (header-carried, offline, DID-bound). But ificaptcha.gitlawb.comrequires interactive human solving, enablingenforcelocks autonomous agents out of repo creation and registration, which are the two core write paths. Worth stating the programmatic acquisition flow in the PR or a linked doc. -
[P3] Pure-shadow replay detection is effectively inert
crates/gitlawb-node/src/icaptcha.rs
Shadow now routes to the read-onlycheck_proof_jti, but shadow never writes the ledger, so in a shadow-only deployment nothing is ever recorded and the replay warning cannot fire. It only does anything in a mixed shadow+enforce setup. Minor, just flagging the limited value of the new path on its own.
Core is solid; the only real gap is the ungated fork path. Once fork_repo is covered (or consciously exempted) and the ledger has tests, this is good to go.
jatmn
left a comment
There was a problem hiding this comment.
[P2] Gate fork_repo, or explicitly document why it is exempt
crates/gitlawb-node/src/api/repos.rs:1420
fork_repo still creates a repository with state.db.create_repo(&record).await?, but unlike create_repo and register its handler does not take HeaderMap and never calls crate::icaptcha::verify_request. In ICAPTCHA_MODE=enforce, an authenticated agent can therefore bypass the new anti-abuse gate by forking any readable repo instead of calling POST /api/v1/repos, even though this route is in the same rate-limited creation route group and produces a new repo row. Please either thread the iCaptcha proof through this path as well, consuming it immediately before db.create_repo, or document the intentional exemption and why fork creation is not part of this abuse boundary.
[P2] Add execution coverage for the replay ledger
crates/gitlawb-node/src/db/mod.rs:1119
The security value of the gate now depends on the new DB-backed single-use ledger, but the committed tests only exercise the pure verifier/decision logic. consume_proof_jti, check_proof_jti, sweep_expired_proofs, and the ProofGuard::consume path are not executed by any committed test even though this file already has a #[sqlx::test] harness. Please add DB-level coverage that proves first consume succeeds, replay returns false, the shadow read-only check does not mutate state, and sweeping removes only expired proof rows.
|
@coderabbitai please do a full review again. approve only if all is good |
|
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/gitlawb-node/src/db/mod.rs`:
- Around line 1132-1143: `check_proof_jti` is currently read-only, so
shadow-mode requests never get recorded and repeated proofs keep appearing
fresh. Update the shadow-mode caller that uses `check_proof_jti` to first record
the proof as consumed on the first seen request, then use the check only to
decide whether to log a would-be replay rejection while still allowing the
request through. Keep the replay-detection behavior centered around
`check_proof_jti` and the downstream shadow path so the first-seen proof is
persisted before subsequent shadow checks.
In `@crates/gitlawb-node/src/icaptcha.rs`:
- Around line 207-224: In `icaptcha::consume`, the shadow-mode branch currently
uses `db.check_proof_jti(...).await?`, which lets transient DB errors bubble up
and fail `register`/`create_repo` even though shadow mode should only log.
Change the `else` path to handle `check_proof_jti` errors locally: log the
failure with `tracing` (including `job.jti`), treat it as non-fatal, and
continue returning `Ok(())` so shadow mode never fails closed; keep the enforced
replay rejection path unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2796eb33-736f-4cc2-8a12-33657b6b2ab0
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (8)
.env.examplecrates/gitlawb-node/Cargo.tomlcrates/gitlawb-node/src/api/register.rscrates/gitlawb-node/src/api/repos.rscrates/gitlawb-node/src/db/mod.rscrates/gitlawb-node/src/error.rscrates/gitlawb-node/src/icaptcha.rscrates/gitlawb-node/src/main.rs
| /// Read-only check if an iCaptcha proof id (`jti`) has already been consumed. | ||
| /// Returns `Ok(true)` if the jti is fresh (not yet consumed), `Ok(false)` if | ||
| /// it is a replay. Used by shadow mode to detect replays without mutating state. | ||
| pub async fn check_proof_jti(&self, jti: &str) -> Result<bool> { | ||
| let exists = sqlx::query_scalar::<_, bool>( | ||
| "SELECT EXISTS(SELECT 1 FROM icaptcha_consumed_proofs WHERE jti = $1)", | ||
| ) | ||
| .bind(jti) | ||
| .fetch_one(&self.pool) | ||
| .await?; | ||
| Ok(!exists) | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Record first-seen proofs in shadow mode before checking replay.
check_proof_jti is read-only, and the downstream shadow path only calls this method, so repeated shadow-mode uses remain absent from icaptcha_consumed_proofs and keep reporting as fresh. That misses the PR goal of logging would-be replay rejections.
Suggested direction
- pub async fn check_proof_jti(&self, jti: &str) -> Result<bool> {
- let exists = sqlx::query_scalar::<_, bool>(
- "SELECT EXISTS(SELECT 1 FROM icaptcha_consumed_proofs WHERE jti = $1)",
- )
- .bind(jti)
- .fetch_one(&self.pool)
- .await?;
- Ok(!exists)
+ pub async fn observe_proof_jti(&self, jti: &str, expires_at: i64) -> Result<bool> {
+ self.consume_proof_jti(jti, expires_at).await
}Then update the shadow caller to log on false but still allow the request.
- let is_fresh = db.check_proof_jti(&job.jti).await?;
+ let is_fresh = db.observe_proof_jti(&job.jti, job.exp).await?;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /// Read-only check if an iCaptcha proof id (`jti`) has already been consumed. | |
| /// Returns `Ok(true)` if the jti is fresh (not yet consumed), `Ok(false)` if | |
| /// it is a replay. Used by shadow mode to detect replays without mutating state. | |
| pub async fn check_proof_jti(&self, jti: &str) -> Result<bool> { | |
| let exists = sqlx::query_scalar::<_, bool>( | |
| "SELECT EXISTS(SELECT 1 FROM icaptcha_consumed_proofs WHERE jti = $1)", | |
| ) | |
| .bind(jti) | |
| .fetch_one(&self.pool) | |
| .await?; | |
| Ok(!exists) | |
| } | |
| /// Read-only check if an iCaptcha proof id (`jti`) has already been consumed. | |
| /// Returns `Ok(true)` if the jti is fresh (not yet consumed), `Ok(false)` if | |
| /// it is a replay. Used by shadow mode to detect replays without mutating state. | |
| pub async fn observe_proof_jti(&self, jti: &str, expires_at: i64) -> Result<bool> { | |
| self.consume_proof_jti(jti, expires_at).await | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/gitlawb-node/src/db/mod.rs` around lines 1132 - 1143,
`check_proof_jti` is currently read-only, so shadow-mode requests never get
recorded and repeated proofs keep appearing fresh. Update the shadow-mode caller
that uses `check_proof_jti` to first record the proof as consumed on the first
seen request, then use the check only to decide whether to log a would-be replay
rejection while still allowing the request through. Keep the replay-detection
behavior centered around `check_proof_jti` and the downstream shadow path so the
first-seen proof is persisted before subsequent shadow checks.
| pub async fn consume(self, db: &crate::db::Db) -> Result<(), AppError> { | ||
| if let Some(job) = self.0 { | ||
| if job.enforce { | ||
| // Enforce mode: consume the jti (mutate state) and reject replays. | ||
| if !db.consume_proof_jti(&job.jti, job.exp).await? { | ||
| return Err(AppError::IcaptchaProofRequired( | ||
| "iCaptcha proof already used (replay); solve a fresh challenge".to_string(), | ||
| )); | ||
| } | ||
| } else { | ||
| // Shadow mode: read-only replay check, log reuse but do not block. | ||
| let is_fresh = db.check_proof_jti(&job.jti).await?; | ||
| if !is_fresh { | ||
| tracing::warn!(jti = %job.jti, "iCaptcha (shadow) proof replay detected"); | ||
| } | ||
| } | ||
| } | ||
| Ok(()) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Don't let shadow mode fail closed on replay-check errors.
Lines 218-219 still propagate db.check_proof_jti(...) failures with ?. Because both gated handlers call proof.consume(&state.db).await?, a transient DB failure will reject register/create_repo in shadow mode even though this mode is supposed to log and still allow.
Suggested fix
pub async fn consume(self, db: &crate::db::Db) -> Result<(), AppError> {
if let Some(job) = self.0 {
if job.enforce {
// Enforce mode: consume the jti (mutate state) and reject replays.
if !db.consume_proof_jti(&job.jti, job.exp).await? {
return Err(AppError::IcaptchaProofRequired(
"iCaptcha proof already used (replay); solve a fresh challenge".to_string(),
));
}
} else {
// Shadow mode: read-only replay check, log reuse but do not block.
- let is_fresh = db.check_proof_jti(&job.jti).await?;
- if !is_fresh {
- tracing::warn!(jti = %job.jti, "iCaptcha (shadow) proof replay detected");
+ match db.check_proof_jti(&job.jti).await {
+ Ok(false) => {
+ tracing::warn!(jti = %job.jti, "iCaptcha (shadow) proof replay detected");
+ }
+ Ok(true) => {}
+ Err(error) => {
+ tracing::warn!(
+ jti = %job.jti,
+ error = %error,
+ "iCaptcha (shadow) replay check failed; allowing request"
+ );
+ }
}
}
}
Ok(())
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| pub async fn consume(self, db: &crate::db::Db) -> Result<(), AppError> { | |
| if let Some(job) = self.0 { | |
| if job.enforce { | |
| // Enforce mode: consume the jti (mutate state) and reject replays. | |
| if !db.consume_proof_jti(&job.jti, job.exp).await? { | |
| return Err(AppError::IcaptchaProofRequired( | |
| "iCaptcha proof already used (replay); solve a fresh challenge".to_string(), | |
| )); | |
| } | |
| } else { | |
| // Shadow mode: read-only replay check, log reuse but do not block. | |
| let is_fresh = db.check_proof_jti(&job.jti).await?; | |
| if !is_fresh { | |
| tracing::warn!(jti = %job.jti, "iCaptcha (shadow) proof replay detected"); | |
| } | |
| } | |
| } | |
| Ok(()) | |
| pub async fn consume(self, db: &crate::db::Db) -> Result<(), AppError> { | |
| if let Some(job) = self.0 { | |
| if job.enforce { | |
| // Enforce mode: consume the jti (mutate state) and reject replays. | |
| if !db.consume_proof_jti(&job.jti, job.exp).await? { | |
| return Err(AppError::IcaptchaProofRequired( | |
| "iCaptcha proof already used (replay); solve a fresh challenge".to_string(), | |
| )); | |
| } | |
| } else { | |
| // Shadow mode: read-only replay check, log reuse but do not block. | |
| match db.check_proof_jti(&job.jti).await { | |
| Ok(false) => { | |
| tracing::warn!(jti = %job.jti, "iCaptcha (shadow) proof replay detected"); | |
| } | |
| Ok(true) => {} | |
| Err(error) => { | |
| tracing::warn!( | |
| jti = %job.jti, | |
| error = %error, | |
| "iCaptcha (shadow) replay check failed; allowing request" | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| Ok(()) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/gitlawb-node/src/icaptcha.rs` around lines 207 - 224, In
`icaptcha::consume`, the shadow-mode branch currently uses
`db.check_proof_jti(...).await?`, which lets transient DB errors bubble up and
fail `register`/`create_repo` even though shadow mode should only log. Change
the `else` path to handle `check_proof_jti` errors locally: log the failure with
`tracing` (including `job.jti`), treat it as non-fatal, and continue returning
`Ok(())` so shadow mode never fails closed; keep the enforced replay rejection
path unchanged.
jatmn
left a comment
There was a problem hiding this comment.
[P2] Complete the request to gate fork_repo or document its exemption
crates/gitlawb-node/src/api/repos.rs:1420
Jatmn's change request is still valid on the current head: fork_repo is in the same rate-limited creation route group as create_repo and register, and it still creates a new repo row with state.db.create_repo(&record).await?, but the handler does not take HeaderMap and never calls crate::icaptcha::verify_request. In ICAPTCHA_MODE=enforce, an authenticated agent can bypass the anti-abuse gate by forking any readable repo instead of calling POST /api/v1/repos. Please complete that review request by threading the iCaptcha proof through this path and consuming it immediately before db.create_repo, or explicitly document why fork creation is outside this abuse boundary.
[P2] Add committed execution coverage for the replay ledger
crates/gitlawb-node/src/db/mod.rs:1119
Jatmn's replay-ledger coverage request is also still open. The committed tests exercise the pure verifier/decision logic, but consume_proof_jti, check_proof_jti, sweep_expired_proofs, and the ProofGuard::consume path are not executed by any committed test even though the file already has a #[sqlx::test] harness. Because the single-use DB ledger is the security boundary that makes proofs non-replayable, please add DB-level tests proving first consume succeeds, replay returns false, the shadow observation path behaves as intended, and sweeping removes only expired proof rows.
[P2] Complete CodeRabbit's request to make shadow replay detection record first-seen proofs
crates/gitlawb-node/src/icaptcha.rs:218
CodeRabbit's current shadow-mode replay request is still valid: the shadow path calls db.check_proof_jti(&job.jti).await?, but that helper is read-only, so a shadow-only deployment never records the first use of a proof. A repeated proof therefore keeps appearing fresh and never produces the "would reject replay" signal that shadow mode is supposed to provide before operators flip to enforce. Please complete that request by recording first-seen proof IDs in the shadow observation path while still allowing the request through.
[P2] Complete CodeRabbit's request to keep shadow replay-check errors non-fatal
crates/gitlawb-node/src/icaptcha.rs:218
The same shadow branch still propagates db.check_proof_jti(...) with ?. Since both gated handlers call proof.consume(&state.db).await?, a transient DB error in shadow mode rejects register or create_repo, even though shadow mode is documented as an observe/log mode that always allows. Please handle replay-check failures locally in the shadow branch, log the jti and error, and return Ok(()) so only enforce mode fails closed on ledger errors.
Summary
Adds an optional iCaptcha "proof-of-intelligence" gate on the two spam-prone
endpoints (
create_repo,register). Callers present an Ed25519-signed proof(earned by solving an escalating challenge at icaptcha.gitlawb.com); the node
verifies it OFFLINE with ed25519-dalek against the published public key and binds
it to the authenticated agent DID so proofs cannot be shared. Motivated by the
did_key spam flood that filled a node's disk.
Impact / compatibility
ICAPTCHA_MODEisoffunless set, so mergingand deploying changes nothing observable. No schema changes, no response-shape
changes, and
git push(receive-pack) is intentionally NOT gated.shadow: verifies and logs would-be rejections, always allows (safe to tune).enforce: rejects create_repo/register without a valid, strong-enough proof.This is the only breaking mode and is opt-in; roll out client proof support and
verify via shadow first.
the gate stays inert rather than rejecting everyone.
Config (env)
ICAPTCHA_MODE= off | shadow | enforce (default off)ICAPTCHA_URL(default https://icaptcha.gitlawb.com)ICAPTCHA_PUBKEY(optional base64url override; else fetched from /v1/pubkey)ICAPTCHA_REQUIRED_LEVEL(default 3)Clients send the proof in the
X-ICaptcha-Proofheader.Testing
cargo test -p gitlawb-node icaptcha— 11 tests pass, including offlineverification of a REAL captured proof (JS-signed, Rust-verified),
tamper/expiry/wrong-DID/insufficient-level/missing-header rejections, and
the off/shadow/enforce/fail-safe mode matrix.
cargo fmt, no new warnings.Cargo.lockdiff is a singlebase64line.Notes / follow-ups
per-DID repo/creation quotas in the node as the absolute ceiling.
Address #100
Summary by CodeRabbit
off,shadow, andenforcemodes plus DID-level validation..env.examplewithICAPTCHA_MODE,ICAPTCHA_URL,ICAPTCHA_REQUIRED_LEVEL, and optionalICAPTCHA_PUBKEY.