mirror of
https://github.com/Twigpine/node.git
synced 2026-11-09 13:31:25 -10:00
feat/commit-status-surface
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dfcaa22b23 |
fix(node): rate-limit repo/agent creation per client IP to stop DID-farm spam floods (#180)
* fix(node): add per-IP flood brake to repo/agent creation endpoints The recurring spam-repo floods are not bypassing the iCaptcha gate — they pass it. The enforced level (3) draws only machine-solvable deterministic challenges (arithmetic/algebra/sequence), and the /v1/challenge API lets the caller pin the easy type, so a bot mints a fresh throwaway did:key, solves a real proof bound to that DID, and creates a repo. The per-DID creation limiter never trips because each DID makes a single create call. Add a per-client-IP rate limiter to the creation route group (repos, register, fork, issues, pulls), reusing the same IpRateLimiter the push path already uses against the identical DID-farm bypass. It wraps the auth layer (outermost) so flood traffic is rejected before signature verification. Default 120/hour/IP, GITLAWB_CREATE_RATE_LIMIT overrides, 0 disables. * docs(env): document GITLAWB_CREATE_RATE_LIMIT in .env.example * fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <[email protected]> * test(node): unit-test the per-IP creation brake runs before auth The route-level coverage (repo_creation_is_rate_limited_by_ip) is #[sqlx::test] and needs Postgres, so it is skipped in the plain unit-test pass. Add a DB-free middleware test that layers the per-IP creation limiter over an always-401 auth stand-in and asserts an exhausted IP is rejected with 429 before auth runs — the ordering the DID-farm flood defense relies on. --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <[email protected]> |
||
|
|
2109d08e2c |
Fix/db startup resilience (#170)
* Keep node alive during database outages * Fix startup-resilience gaps: reachable 503s, bounded retries, seamless handoff - Route sqlx errors buried in anyhow chains back to AppError::Db and widen the unavailable class (Io/Tls/pool), so DB outages return 503 instead of 500 — the previous 503 arm was unreachable dead code - Bound each connect+migrate attempt (GITLAWB_DB_CONNECT_TIMEOUT_SECS) so a held migration advisory lock cannot wedge the startup retry loop forever - Share one bound socket between the degraded and full servers and stop awaiting the degraded drain: no closed-port window during the handoff and no slow client can block the full server from starting - Probe the database from /ready (2s bound) and gate Fly routing/deploys on it, so mid-life outages drain traffic and a bad DATABASE_URL fails the deploy visibly instead of going green; classify auth/catalog errors as likely-permanent (error-level log, straight to max backoff) - Start the metrics listener before the DB connects so the degraded window is observable; degraded /health now returns 503 so peer pings and uptime monitors stop counting a DB-less node as alive - Move the five GITLAWB_DB_* knobs onto clap Config (validated, in --help), drop the redundant outer connect timeout, default the pool to 20 conns, fix the backoff exponent cap that defeated GITLAWB_DB_RETRY_MAX_SECS, and strip at most one .git suffix in smart-HTTP repo names * fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <[email protected]> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <[email protected]> |
||
|
|
cd67718f49 |
fix(node,git): bound a hung served git with a total-duration timeout (#62) (#165)
* feat(node,git): bound a hung served git with a total-duration timeout (#62)
Part 1 of #62. A served git upload-pack/receive-pack that neither finishes
nor disconnects parks forever, pinning a git PID (and, for receive-pack,
holding the write lock); on a public repo an anonymous caller can trigger
it. No tower TimeoutLayer exists and axum imposes no default.
Wrap the child interaction in tokio::time::timeout, bounded by a new
GITLAWB_GIT_SERVICE_TIMEOUT_SECS knob (default 600, must be positive).
Drain stdout/stderr concurrently with the stdin write so a large body
can't deadlock. On expiry run_git_service returns a typed GitServiceTimeout
and reaps the WHOLE process group before returning (SIGTERM so git clears
its .git/*.lock, wait for the leader and its grandchildren like index-pack
to exit, escalate to SIGKILL past a grace, hard-capped), so a caller
releasing the receive-pack write lock can't race a still-live git on the
same repo.
The handlers map GitServiceTimeout to a distinct 504 (not the generic 500
git error, and clear of the read-gate 404 / auth 401 the client keys
retries on) via a pure, unit-testable classifier rather than matching the
anyhow string. Surface git's own non-zero exit (its stderr) before a
stdin-write EPIPE so a malformed body is classified 400, not 500.
Out of scope, deferred in #62: the info/refs advertisement and the
withheld-blob path (spawn_blocking, which tokio timeout cannot cancel);
both remain unbounded (noted in the config knob's docs).
Tests: the timeout fires and tears the group down; it reaps the whole
group before returning (RED if the reap is leader-only); the wrap is
load-bearing (RED via an outer bound, not a hang); the error must be the
typed timeout; the 504 mapping and classifier are unit-tested; config
rejects 0. Full suite green.
* fix(review): harden #62 timeout tests and drop a dead reap flag
- test(node): cover the `protocol error` arm of git_service_app_error
independently (the input has no "bad line length" substring, so it
isolates the second classifier arm and goes RED if that arm is removed)
- test(node): make the reap-before-return test's grandchild IGNORE SIGTERM
(`trap "" TERM`) and outlive the ~4s reap cap, so the SIGKILL escalation
is load-bearing. Previously the grandchild self-exited under the cap, so
neutering the SIGKILL still passed; the escalation was exercised but not
required. Verified: RED with SIGKILL disabled, GREEN with it.
- test(node): add a receive-pack timeout test. Every prior timeout test used
an upload-pack fake; this proves the push path (which also holds the repo
write lock) is bounded too. RED-verified with the internal timeout removed.
- test(node): poll for the pidfile in the reap-before-return timeout test
so a loaded runner can't false-panic before the fake git writes it
- remove the redundant `sigkilled` guard in reap_group_on_timeout; step
== 200 fires exactly once in the 0..400 loop, so no re-entry guard is
needed and the SIGKILL still fires exactly once
* fix(node): log malformed receive-pack as warn, not error (#165)
git_receive_pack's error classifier routed every non-Timeout error,
including client-caused BadRequest (400), through tracing::error!. Mirror
the git_upload_pack arm so a malformed push logs at warn level instead of
as a server error.
* test(node,git): make smart_http timeout tests pass under the parallel runner (#165)
Addresses jatmn's review of
|
||
|
|
d00d89ae5b |
fix(node): gate POST /api/v1/sync/trigger and rate-limit the peer-sync routes (#82) (#161)
* feat(node): add per-client-IP limiters + knobs for the peer-sync routes (#82) Plumb two dedicated per-IP RateLimiters into config/state so the peer-sync enqueue routes can be braked independently: sync_trigger_rate_limiter (tight, GITLAWB_SYNC_TRIGGER_RATE_LIMIT, default 60/hr) and peer_write_rate_limiter (generous, GITLAWB_PEER_WRITE_RATE_LIMIT, default 600/hr). Both are bounded and swept by the periodic cleanup loop, keyed via the node-wide push_limiter_trust. No route is wired to them yet; that lands next. * fix(node): require a signature on /sync/trigger and rate-limit the peer-sync routes (#82) /api/v1/sync/trigger was reachable unauthenticated in the default config and drove an O(peers) outbound fan-out + per-repo sync_queue enqueue — an anonymous amplification/DoS. The same enqueue_sink is reachable via /sync/notify, which also accepts unsigned known-peer requests and had no rate limit. - Move /sync/trigger into its own always-signed route group (add_auth_layers), independent of require_signed_peer_writes, and give it a tight per-IP brake. - Add a separate, generous per-IP brake to the peer-write group (/peers/announce, /sync/notify); notify's signature behavior is unchanged (rolling-upgrade compat), it only gains the brake. Separate buckets so an unsigned notify flood can't drain the signed trigger caller's quota. - Per-IP (not per-DID): a did:key farm self-registers, so a signature/per-DID gate does not cap cost (INV-10); the brake keys on the client IP before auth. Adds an adversarial test matrix (unsigned->401 both modes, IP flood->429, DID-agnostic brake, forged X-Forwarded-For can't bypass, notify flood->429, distinct-IP not throttled, cross-route non-exhaustion, 0-disables-but-sig-holds, announce still accepts unsigned). * fix(gl): surface node denial on sync trigger and sanitize the error body (#82) Now that the node gates /api/v1/sync/trigger behind a signature and rate limit, 'gl sync trigger' must not render a 401/429 as a fabricated '✓ sync triggered / 0 peers' success. Check resp.status() before parsing; on a non-2xx, extract the node's message and bail. Strip C0/C1 control bytes from the node-supplied string before printing (INV-6) — a hostile node could embed ANSI/OSC escapes in the body. Adds mockito tests: 401/429 surface as errors (RED without the status check), control bytes are stripped, and the 200 path is unchanged. * fix(review): bound gl error-body read, strip bidi controls, genericize 429 message (#82) Code-review follow-ups on the #82 change: - gl sync trigger: read the node error body with a capped reader (read_body_capped) instead of resp.text(), so a hostile/broken node can't force an unbounded allocation to surface a denial (INV-6, read half). - sanitize_node_msg: also strip Unicode bidi/format controls (U+202A-202E, U+2066-2069, LRM/RLM/ALM) — is_control() only covers the Cc category, but a right-to-left override can reorder the displayed error line. - too_many_requests(): the shared per-IP 429 middleware now serves the peer-sync routes too, so drop the push-specific wording ('rate limit exceeded'); the push-specific 429 in the receive-pack handler is unchanged. Adds tests for the bidi/length sanitize and the oversized-body path. * test(#82): close reasoned-not-run gaps with load-bearing tests Executes the branches that were previously only reasoned: - read_body_capped byte-cap test (RED against resp.text() — 2MB body -> ≤cap): vets the bounded-read fix, not just the display cap. - sync-route 429 body asserts 'rate limit exceeded' and NOT 'push' (RED before the message was genericized); also finishes genericizing the warn log, which the response-body change had missed. - real RFC 9421 signature through the full router reaches the handler (200) in both config modes — the positive path through the actual gate, not a direct-mount with an injected DID. - /peers/announce flood -> 429 (the peer_write brake covers announce, not just notify). - trigger_counts extraction unit test (happy + malformed -> 0,0), closing the count-formatting gap in the 200 path. No production behavior change beyond the warn-log wording. * fix(#82): require an identity for sync trigger and fix the rollout docs Address review feedback on the always-signed /sync/trigger route. - gl sync trigger now requires a local identity via load_keypair_from_dir and fails locally with "identity not found" before issuing a request, instead of .ok() silently sending an unsigned request that can only 401 remotely. Add a test proving it fails before dialing the node. - Split /sync/trigger out of the staged unsigned peer-write rollout in the README and the OSS-readiness audit; only /peers/announce and /sync/notify keep the rolling-upgrade unsigned behavior. - Reorder the two peer-sync rate-limit vars in .env.example for dotenv-linter. - Bump crossbeam-epoch to 0.9.20 (RUSTSEC-2026-0204), matching main, so cargo audit passes. --------- Co-authored-by: t <t@t> |
||
|
|
2df6ff9d30 |
fix(node): close two spam-vector root causes (trust upsert + ungated push) (#152)
* fix(node): close two spam-vector root causes (trust upsert + ungated push) The June 2026 spam wave kept re-materializing after cleanup because two paths let an unauthenticated-by-captcha actor keep operating: 1. `update_trust_score` was an upsert. Any authenticated push/issue/PR from a DID with no agent row silently INSERTed one with a fresh `registered_at`, bypassing the iCaptcha gate on /api/register — so deregistered spam DIDs came back the moment they pushed. Make it an UPDATE-only no-op for unregistered DIDs; registration is the sole way into the agents table. Regression test added. 2. `git-receive-pack` had no rate limit. The per-DID limiter on the creation routes can't brake a push flood from a DID farm (one throwaway identity per repo), so the node absorbed several pushes/sec per IP, each a Tigris round-trip + peer-notify fan-out. Add a per- client-IP limiter (Fly-Client-IP / X-Forwarded-For) on the push path, default 600/h, `GITLAWB_PUSH_RATE_LIMIT` override, 0 disables. Fails open without a proxy header so self-hosted nodes aren't broken. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(node): address PR review — advertisement throttle, trusted-proxy IP key, bounded limiter Resolves the review findings on the push rate limiter: P1 — advertisement path bypassed the limiter. A push hits GET info/refs?service=git-receive-pack first, which calls acquire_fresh() (a Tigris download); it sat in git_read_routes under only optional_signature, so the flood was reachable via the cheap GET without ever sending the throttled POST. Now git_info_refs applies the same per-IP brake before the fresh acquire. P1 — client-supplied forwarded headers were trusted. The key came from Fly-Client-IP / the first X-Forwarded-For hop, both client-settable, so a flooder could rotate the header and never fill a bucket. Replaced with a TrustedProxy policy (GITLAWB_TRUSTED_PROXY = fly | x-forwarded-for | unset): trust only the operator's edge header, fall back to the real socket peer (ConnectInfo) otherwise. Fly config trusts Fly-Client-IP; the AWS/Caddy compose trusts the rightmost X-Forwarded-For hop. P2 — unbounded limiter key set / insert-before-reject. Added a max_keys cap (reject-before-insert, inline eviction when full) so a varied key cannot grow the map, and a rejected request never allocates. Applied to both the per-IP and per-DID limiters. P2 — malformed forwarded header disabled the brake. Empty leading XFF hop returned None and skipped the limiter; empty Fly-Client-IP returned Some(""). client_key now takes the rightmost XFF hop and always falls back to the peer, so a malformed header can never turn the limiter off. P2 — missing tests. Added: middleware 429 path, per-peer isolation, key-cap eviction/rejection, trusted-proxy header resolution, and an integration test asserting the receive-pack advertisement is throttled before the acquire. Also serve with into_make_service_with_connect_info so the peer address is available. New env: GITLAWB_PUSH_RATE_LIMIT, GITLAWB_TRUSTED_PROXY (documented in .env.example). Co-Authored-By: Claude Opus 4.8 <[email protected]> * Update infra/aws/compose.yaml.tftpl Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix(node): amortize the rate limiter's capacity eviction scan `RateLimiter::check` ran a full O(max_keys) `retain` sweep on every new-key miss once the map was at capacity — which is exactly the distinct-key-flood state the cap defends against, so the limiter serialized all traffic behind a 200k-entry scan per request under the mutex. Gate the inline sweep to run at most once per `sweep_interval` (min of the window and 1s), tracked by `last_sweep`. Between sweeps a new key is rejected without scanning; the background `cleanup()` loop still reclaims independently. The fast path for existing keys is unchanged, the key cap invariant holds (map never exceeds max_keys, rejected keys never allocate), and post-flood self-healing stays bounded to one interval. Added a test asserting a burst of capacity misses does not trigger repeated eviction scans. Co-Authored-By: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: Claude Opus 4.8 <[email protected]> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> |
||
|
|
adc20f9eff |
feat(node): iCaptcha proof-of-intelligence gate on create_repo + register (#108)
* feat(node): iCaptcha proof-of-intelligence gate on create_repo + register 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 <[email protected]> * fix(node): address review on icaptcha gate (validate config, bound key 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 <[email protected]> * fix(node): consume iCaptcha proof jti so a proof can't be replayed 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 <[email protected]> * fix(node): consume iCaptcha proof only after request passes validation 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 <[email protected]> * fix(node): give iCaptcha rejections a dedicated error code 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 <[email protected]> * fix(node): gate fork_repo with iCaptcha and cover the proof ledger Address remaining PR #108 review comments: - fork_repo was the third repo-creation entrypoint and bypassed the iCaptcha gate. Verify the proof up front (fail fast on missing/invalid) and consume it only just before the git clone, so cheap rejections (bad name, conflict, withheld subtree) never burn a valid proof — matching create_repo's verify/consume split. - Add #[sqlx::test] coverage for the single-use ledger that the gate's anti-replay value depends on: consume_proof_jti (first-use vs replay vs distinct jti) and sweep_expired_proofs (only expired rows removed, unexpired spent proofs still block replays). - Return 403 Forbidden (was 401) for IcaptchaProofRequired: the caller is authenticated but forbidden without a valid proof. The distinct icaptcha_proof_required code is unchanged. - Recommend pinning ICAPTCHA_PUBKEY in production in .env.example to avoid the startup fetch dependency. The two shadow-mode review comments were already resolved by the earlier verify/consume refactor (decide/verify are IO-free; check_proof_jti gone). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> |
||
|
|
0a15e763d2 |
feat(node): owner-only push enforcement behind GITLAWB_ENFORCE_OWNER_PUSH (#31) (#68)
* feat(node): owner-only push enforcement behind GITLAWB_ENFORCE_OWNER_PUSH (#31) A valid did:key signature on git-receive-pack is authentication, not authorization: any party can generate a key, derive its DID, sign, and push to an unprotected branch. Owner checks only ran on protected branches, so a non-owner could write to unprotected branches of an arbitrary repo. Add an opt-in flag GITLAWB_ENFORCE_OWNER_PUSH (default false, mirroring GITLAWB_REQUIRE_SIGNED_PEER_WRITES) that requires the authenticated pusher to be the repo owner on every branch. Default-off keeps live nodes unaffected. Design follows the maintainer's review on #31: - Authorize on the canonical AuthenticatedDid injected by require_signature, via Extension<AuthenticatedDid> on the handler — not a re-parse of the headers. The legacy branch-protection check is left as-is. - Fail closed: an absent identity or a non-owner caller is rejected. The policy lives in a pure fn (owner_push_rejection) so it is unit-tested without a DB. - The gate runs before branch protection, so one rejection yields one error body. - Factor the duplicated owner-match into auth::is_repo_owner, reused by receive-pack, protect.rs, and visibility.rs (each keeps its own message). events.rs / peers.rs are left out — they aren't owner checks. - Name the push gate by intent (caller_authorized_to_push) so the planned Phase 2 UCAN git/push capability is a pure addition, not a rewrite. Tests: owner (full + short DID) allowed, non-owner rejected, missing-DID rejected, flag-off legacy. Docs: .env.example + docs/RUN-A-NODE.md. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(node): address review on #68 — 403 status, finish owner-match consolidation Per @beardthelion's review: - Return 403 Forbidden (not 400) for authorization denials: the owner-push gate, the branch-protection denial, and the owner checks in protect.rs / visibility.rs. 400 reads as a malformed request and some git/CI clients retry it. - Finish the is_repo_owner consolidation: route the branch-protection check and the repos list filter through the helper. The branch-protection check now uses the verified AuthenticatedDid (auth.0) instead of re-parsing the headers. - Drop the second identity source on the push path entirely: the post-push event recording also uses auth.0, so extract_did_from_auth (and the handler's HeaderMap param + import) are removed. - Tests assert the Forbidden variant (so a future status regression fails loudly) and add direct coverage for is_repo_owner / caller_authorized_to_push. - Docs: warn that enabling the flag blocks delegated and CI agents until the Phase 2 UCAN git/push capability is honored. Co-Authored-By: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: FrozenRaspberry <[email protected]> Co-authored-by: Claude Opus 4.8 <[email protected]> |
||
|
|
0b056f3ad0 |
[codex] fix Dependabot Rust alerts (#15)
* fix(deps): resolve dependabot rust alerts * docs: clarify dependabot validation |
||
|
|
d366fc6b97 |
[codex] chore: add live-safe OSS readiness pass (#14)
* chore: add live-safe OSS readiness pass * fix(node): verify optional peer signatures during rollout * docs: rewrite README for node roadmap |
||
|
|
6b9c5278d4 |
chore: initial public release
Open-source release of the gitlawb node, CLI, and git remote helper. - gitlawb-node: Axum HTTP server with git smart-HTTP and DID auth - gl: command-line client (identity, repos, PRs, MCP server) - git-remote-gitlawb: git remote helper for gitlawb:// URLs - gitlawb-core: shared crypto primitives (DID, CID, HTTP sigs, UCAN) Dual licensed under MIT or Apache-2.0. Co-Authored-By: OpenClaude <[email protected]> |