mirror of
https://github.com/Twigpine/node.git
synced 2026-11-08 17:55:53 -10:00
main
20
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c805e35f40 |
fix(node): bound GraphQL query cost (#552)
* fix(node): bound GraphQL query cost * fix(node)!: bound repo names so emitted cursors always parse The reposPage cursor embeds base64(JSON(owner_did, name)) while parse_repo_cursor rejects cursors over 4096 bytes, but repo names were unbounded on every write path. A single long-named visible repo made the server emit an endCursor it then rejected as "invalid repository cursor", dead-ending pagination for every caller past that row. Bound names to MAX_REPO_NAME_LEN (100 bytes) at the create_repo and fork_repo handlers via a shared validate_repo_name helper (bad charset is still a 400), and reject overlong peer-supplied names in Db::upsert_mirror_repo (the sync caller already drops the error and skips the mirror). Also in this round: - docs/graphql-pagination.md states the 200 page-size maximum and corrects the over-budget remedy: a smaller limit does not reduce the reposPage or repos price, which is charged at the full MAX_VISIBLE_REPO_PAGE_SIZE scan regardless of limit. - Tests pin refUpdates(limit: -1) clamping to zero rows, the parse_repo_cursor wrong-shape-JSON arm with its exact "invalid repository cursor" message, the longest-admissible-name cursor round-trip, and the mirror write-path rejection. BREAKING CHANGE: the GraphQL `repos` field now returns an error instead of the full list when more than MAX_VISIBLE_REPO_PAGE_SIZE (200) repositories are visible; larger lists must use `reposPage` with `limit`/`after`. Repository names are additionally limited to 100 bytes at creation, fork, and mirror-registration time. * fix(node): address beardthelion P3 findings on name bound and input messages - Assert each rejection's own message in the reposPage invalid-inputs loop: the loop no longer accepts either message for every query, so a misattributed message (limit arm returning the cursor message) goes red. - Pin the create/fork name-bound wiring through the handler: new test drives create_repo with an overlong name via test_state_lazy and asserts the 400; deleting the validate_repo_name call site would proceed to the lazy DB pool and fail with a connection error instead. - Document the API-side vs disk-side validate_repo_name divergence on the new function, and tie the store's literal 100 to MAX_REPO_NAME_LEN so the two validators cannot drift apart on length. --------- Co-authored-by: Marlowe <[email protected]> Co-authored-by: cairn-intern <[email protected]> |
||
|
|
0366f18458 |
fix(node): reconcile public-read messaging with per-repo enforcement (#439)
* fix(node): reconcile public-read messaging with per-repo enforcement GITLAWB_PUBLIC_READ is read nowhere except a startup warning, while read enforcement actually happens per repository through is_public and path-scoped visibility rules. The flag's help text, the startup warning, .env.example, and the readiness audit all still claimed private-read enforcement was unwired. The help and warning now state the flag is reserved and inert, and point at the real per-repo control. A config test asserts the rendered help neither claims enforcement is missing nor omits that the flag does nothing. Closes #338. * docs: drop the stale private-read priority entry * chore: refresh PR head |
||
|
|
c97695a259 |
Add a completeness fence over the GraphQL mutation surface (#219)
* test(node): add a completeness fence over the GraphQL mutation surface Gate the GraphQL mutation surface the way REST is gated, as a SEPARATE guard with its own bucket table (a guard over a heterogeneous set must classify per member, INV-1). every_graphql_mutation_has_its_gate asserts the mutation set is complete (every mutation classified) and that each carries its gate marker. Completeness is derived from async-graphql's own introspection: the field names come from the engine as structured data, so no source-text or SDL-format shape (a brace or async fn in a comment or string, a whitespace/generic/skip variant, a brace in a rustdoc description) can hide a live mutation. A new unregistered mutation, including a repo-write, fails the build. The marker check (does each resolver body call its gate) is a best-effort static backstop, bounded to each resolver's own comment-masked, string-aware body. It is explicitly not the completeness guarantee and not load-bearingness: a marker inside a string literal is a documented vacuity residual, and the adversarial sqlx tests in graphql/mutation.rs are the real proof the gates reject. Verified by execution: an unregistered mutation reddens (unclassified), a removed did_matches gate reddens (missing marker), the clean tree is green; no DB, no feature flag. * docs(node): reconcile GraphQL mutation-auth claims with the shipped state README's known limitations, the OSS-readiness live-network blockers, and the maintainer roadmap still listed GraphQL mutation auth as an open item, which contradicts #87: GraphQL mutations already require a verified signer bound to the acting DID. Reconcile each to the shipped reality while keeping the genuinely-open nuance: the GraphQL mutation surface is the agent-task queue, repo writes stay REST-only, and no owner-gated repo-write mutation is exposed over GraphQL. The every_graphql_mutation_has_its_gate fence forces any new mutation to be classified in its table; owner-gate and REST parity stay the INV-1 review check. * fix(node): source the GraphQL mutation fence from SDL, not introspection Resolves jatmn's #219 P1. The every_graphql_mutation_has_its_gate completeness fence enumerated mutation fields via async-graphql introspection, which filters out `#[graphql(visible = false)]` fields. Those fields stay executable, so a future hidden mutation would need no registry row and no marker while both set-equality checks stayed green -- defeating the fence's guarantee that no live mutation goes unclassified. (Confirmed by execution: a synthetic visible=false mutation is absent from introspection, executable, and present in schema.sdl().) Source completeness from the engine's own SDL instead. A new pure helper mutation_field_names_from_sdl parses schema.sdl() with async-graphql's parser AST (async_graphql::parser::parse_schema) and reads the mutation type's fields; SDL includes visible=false fields and the AST parse is immune to a brace in a field description. Introspection is kept as a cross-check: any field present in SDL but hidden from introspection now fails named as a forbidden hidden mutation. The stale doc comments (which claimed introspection could not hide a field) are corrected. A self-checking version-safety test builds a synthetic schema with a visible=false mutation and asserts the SDL-sourced set includes it while introspection omits it -- so if a future async-graphql ever filters SDL by visibility, that test goes RED rather than the gap reopening silently. Load-bearing verified by execution: stubbing the SDL enumeration to empty turns four tests RED, including the fence itself; restoring returns green. Only api/mod.rs changed; the mutation resolvers are untouched. `cargo test -p gitlawb-node --bins` green (495); clippy and fmt clean. * fix(node): make the SDL mutation-fence wiring load-bearing; admit deprecated An adversarial review of the SDL switch found three gaps: - The fence's completeness source (SDL) and its hidden-mutation cross-check were not load-bearing: because the real MutationRoot has no visible=false field, reverting `fields` back to introspection and dropping the cross-check left the whole suite green (INV-21). Extract the sourcing + cross-check into a generic `assert_mutation_fence<Q,M,S>` that the real fence AND a synthetic hidden schema both drive. A new #[should_panic] test (fence_rejects_a_hidden_mutation) feeds a visible=false mutation that IS registered, so only the cross-check can fire; reverting the SDL source or deleting the cross-check now changes/removes that panic and turns the test RED (verified). - The introspection cross-check used bare `fields { name }` (GraphQL default includeDeprecated:false), so a `#[graphql(deprecation = "...")]` mutation -- executable and present in SDL -- would false-red as a hidden visible=false mutation. Use `fields(includeDeprecated: true)`; a new test (fence_admits_a_deprecated_mutation) pins it, going RED without the flag. - schema_lists_exactly_the_registered_mutations still sourced from introspection and its comment called introspection "the source of truth", contradicting the fix. Retargeted at the SDL set with a corrected comment. Also folds the two introspection navigations into one generic helper (introspection_mutation_fields) and points the version-safety test at it, so there is a single introspection query. cargo test -p gitlawb-node --bins green (497); clippy and fmt clean. Only api/mod.rs changed. * fix(node): rebase #219 and require signer gate in the GraphQL fence Rebase onto current main and skip the obsolete 0.5.1 lockfile commit. The mutation fence now requires both require_signer(ctx)? and did_matches( in every registered resolver, and each_mutation_requires_and_binds_signer covers all four task mutations for unsigned and DID-mismatch denial. * docs(node): reconcile GraphQL fence docs with SDL discovery Restore the historical OSS audit blocker text and add a dated status update for #219. README and the maintainer roadmap now describe the SDL-sourced fence and signer-plus-binding markers. * ci: trigger the PR checks on a new head |
||
|
|
e2ae21795a |
chore: rebrand user-facing gitlawb references to Twigpine (#520)
* chore: rebrand user-facing gitlawb references to Twigpine
Rename the product name in docs, README, CLI help/output, MCP tool
descriptions, macOS app strings, installers, npm metadata and license
headers from gitlawb/Gitlawb to Twigpine. Point GitHub links at the
renamed Twigpine org (and fix the release workflow's repository guard,
which still matched Gitlawb/node), and switch the public node hosts and
website/install URLs to twigpine.com.
Protocol and operator identifiers are intentionally unchanged
(did:gitlawb, gitlawb://, signing domains, GITLAWB_* env vars, crate
and binary names, X-Gitlawb-* headers, icaptcha and peer-network URLs);
they need a follow-up migration.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* docs: point image pulls at ghcr.io/twigpine/node and fix macOS DMG name
The release workflow publishes the container image to
ghcr.io/${GITHUB_REPOSITORY,,}, which is ghcr.io/twigpine/node since the
org rename; the old ghcr.io/gitlawb/node path no longer resolves for
anonymous pulls. Point the RUN-A-NODE docker run example, the OSS
readiness audit's claim about it, and the macOS app's generated compose
file at the published path.
The README listed the macOS DMG as "Twigpine Node.dmg", but
build-macos-app.sh writes GitlawbNode-macOS.dmg (kept on purpose), so
document the real file name.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix(infra): keep the AWS security group description to avoid replacement
`description` on aws_security_group is ForceNew, so renaming it forced a
replace of the in-use group on the next apply of an existing deployment.
Restore the original text and note why it must stay until a separate
infra migration.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix(macos): regenerate the app's compose file instead of treating it as a repo's
Config.repoPath fell back to the data directory when no repo clone was
found, and DockerCompose then picked up the compose file it had generated
there as if it belonged to a repo. The generator therefore only ran on
first launch, so upgraded installs kept their original compose, including
the old ghcr.io/gitlawb/node image, which no longer serves anonymous pulls.
repoPath now returns nil when there is no clone, and the app regenerates
its compose file whenever no repo compose is in use. Project directory and
named volumes are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix(infra): default the AWS node image to ghcr.io/twigpine/node
release.yml publishes to ghcr.io/${GITHUB_REPOSITORY,,}, which is now
ghcr.io/twigpine/node; ghcr.io/gitlawb/node denies anonymous pulls, so the
default made new deployments fail to start and the upgrade command fail
on existing ones. On an existing deployment `terraform apply` only updates
the upgrade SSM document in place (user_data drift is ignored); the new
image is pulled on the next upgrade_command.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix(macos): keep operator edits to the generated compose, migrate only the image
|
||
|
|
73fd7473af |
Merge pull request #330 from Gitlawb/fix/default-enforce-owner-push
fix(node)!: enforce owner-only push by default |
||
|
|
21f8cb6135 |
Merge pull request #173 from Gitlawb/fix/issue-135-ipfs-cid-tree-gate
fix(node): gate GET /ipfs/{cid} tree objects so a withheld subtree's structure can't leak (#135)
|
||
|
|
de4d932609 |
docs(infra,macos): make the opt-out reachable and stop the AWS guide contradicting itself
All four are in the deployment half added last round; none is in the Rust. The AWS guide told operators two different things -------------------------------------------------- "Changing configuration" still said editing `image_tag` does not affect a running instance and routed to `-replace`, while the upgrade section thirty lines above now reinstalls the rendered compose, which carries `image_tag`. The same paragraph told operators to edit `.env` and restart — which the new "Why the upgrade rewrites the compose file" section explains is silently ineffective for any key the installed compose does not already name. Split by which file carries the change rather than deleted, because the distinction is real: values rendered into `compose.yaml` (`image_tag`, ports, `domain_name`, `db_host`, `icaptcha_*`) roll out with `apply` + `upgrade_command`; values user-data writes into `.env` at first boot (`public_url`, `bootstrap_peers`, integrations) do not propagate from tfvars at all and need an edit on the instance or a replacement. The third case is the one that bites: a setting the installed compose does not yet name is read from `.env` and dropped, so it needs the upgrade first. `--remove-orphans` stopped being inert --------------------------------------- The flag is not new, but until the upgrade rewrote `compose.yaml` it reconciled against the instance's own file, so the only orphans were hand-started containers. It now reconciles against a fresh rendering whose service set is conditional — `postgres` only when `db_host` is empty, `caddy` only when `domain_name` is set. Setting `use_rds` or clearing `domain_name` and then running what the README calls a pull-and-restart stops and removes the local postgres or the TLS terminator. That is what those variables mean, but it happens on the upgrade rather than on apply, so it is now stated next to the Terraform-owned warning. Data survives either way; both bind-mount under /mnt/data. The macOS setting had no writer -------------------------------- `enforceOwnerPush` was persisted, loaded, emitted into `.env`, and forwarded by the generated compose — and nothing set it. `SettingsWindow` had no state and no assignment, so the only way to reach the opt-out on that launch path was hand editing config.json in Application Support, which no document mentions. Editing `.env` does not work there either: `writeEnvFile()` rebuilds it from `Config` on every start. It now has a Toggle beside "Sync repos from peers", following the pattern already in that view. Three lines the change made untrue ----------------------------------- The handler test asserted "with no configuration at all" while setting `enforce_owner_push` explicitly — the right thing for the test, the wrong claim for the message. The cross-tenant docstring still described three distinct pusher DIDs after A and B were given `n1`'s owner. And OSS-READINESS-AUDIT listed unprotected branches not enforcing owner checks as a live blocker; owner checks now cover every branch, and only the UCAN half of that sentence still stands. Also dropped the `#[allow(dead_code)]` on `test_state_with`, redundant since the same commit added its call site. |
||
|
|
0b47e1fc08 | docs: clarify visibility limits | ||
|
|
cdbda19c29 |
fix(node)!: enforce owner-only push by default
A `did:key` signature is authentication, not authorization: the method is self-certifying, so any party can generate a keypair, derive its DID and sign. With `GITLAWB_ENFORCE_OWNER_PUSH` defaulting to `false`, `owner_push_rejection` short-circuited and every `git-receive-pack` carrying any valid signature was accepted — including pushes to a repository the signer does not own, and including private ones, on every branch not explicitly protected. The gate itself already worked when switched on. What was missing is that no reachable default configuration switched it on, so a node started with nothing configured accepted a push from anyone. That default is what this changes. The argument now takes a value so there is a way back: `--enforce-owner-push false` and `GITLAWB_ENFORCE_OWNER_PUSH=false` both disable it, while the bare `--enforce-owner-push` form still means `true`. Without this the flag would be presence-only (`ArgAction::SetTrue`), which with a `true` default would leave operators no way to opt out during a rolling upgrade. Three concurrency tests pushed as non-owners in order to reach the shedding logic they actually assert on. They now push as the repo owner, so they keep exercising the shipped default rather than opting out of the gate. Docs updated in README.md, .env.example and docs/RUN-A-NODE.md, including the delegated-key caveat below. README also carries a dated cutover for `GITLAWB_REQUIRE_SIGNED_PEER_WRITES`, which stays `false` for now so live peers can finish upgrading. BREAKING CHANGE: `git-receive-pack` now rejects a push whose authenticated DID is not the repo owner, returning 403 before any ref update is applied. Delegated and CI keys count as non-owners: a UCAN `git/push` capability is verified but not yet honored for authorization, so an agent pushing under its own DID cannot push while this is on. Set `GITLAWB_ENFORCE_OWNER_PUSH=false` during a rolling upgrade, or have automation push as the repo owner, until scoped collaborator / UCAN-delegated push rights land. |
||
|
|
f4f0f4bff0 | Merge remote-tracking branch 'origin/main' into fix/issue-135-ipfs-cid-tree-gate | ||
|
|
9f340eac5e |
Merge upstream/main into fix/issue-210-stable-advisory-lock-key
Resolve conflicts: keep the SHA-256 advisory-lock key change alongside upstream's connection-affine RepoWriteGuard rework and its F1/F3b/F4/U8 test suite; RUN-A-NODE.md keeps both the updated Public URL row and the rolling-upgrade advisory-lock row. |
||
|
|
8f3a52175a |
docs(node): correct the walk-cap and db-pool operator docs
Two doc drifts jatmn flagged on #173, both docs-only. The /ipfs walk cap is min(internal ceiling, GITLAWB_IPFS_MAX_REPOS_WALKED), where the internal ceiling is MAX_PIN_SOURCES + 1 = 17. config.rs, README, and .env.example all described a max() floor instead, so an operator reading them would think the default of 64 was the effective ceiling and that raising the knob bought headroom. Neither is true. The runtime is not changed: two tests set the knob to 1 and depend on the cap actually being 1, so wiring the documented floor would make the knob inert and break them. db_max_connections has defaulted to 48 for a while and four comments still said 20. Two of them did arithmetic on it. main.rs and repo_store.rs justified the separate advisory-lock pool by "20 is below 32", which is now false and made the separation look like a consequence of the defaults; it is structural, since a push holds its connection for the whole receive-pack at any pool size. Also adds a RUN-A-NODE troubleshooting entry for the boot failure an in-place upgrade hits when it is still configured with the old pool size of 20. |
||
|
|
547f9aed10 | fix(node): preserve legacy peer compatibility | ||
|
|
89e10d0ff1 |
fix(repo_store): use SHA-256 for stable advisory-lock key (#210)
DefaultHasher is not frozen by the Rust standard and has shifted across toolchain versions, so the same (owner_slug, repo_name) produced a different i64 advisory-lock key on different builds and broke cross-machine write-exclusion for the same repo. Hash with SHA-256 instead and pin the result with a golden-value test, plus a one-axis-at-a-time differential test so a regression that drops either parameter is caught. Document the SHA-256 re-keying caveat on acquire_write and in the RUN-A-NODE.md operator checklist (drain writes or cut over single-node during a rolling upgrade). |
||
|
|
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> |
||
|
|
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]> |
||
|
|
9e6e832ec5 |
Infra reorg (#27)
* refactor: move fly.toml into infra/fly/ Deployment configs now live under infra/, organized per target. Dockerfiles and docker-compose.yml intentionally stay at the repo root (shared by CI, build scripts, and the macOS app). Deploy with: fly deploy -c infra/fly/fly.toml Co-Authored-By: OpenClaude <[email protected]> * feat(infra): Terraform for single-EC2 AWS deployment infra/aws/ provisions a production-leaning single-node deployment: - t4g.small (AL2023 arm64) running the published ghcr.io/gitlawb/node image + postgres:16 via docker compose - separate encrypted EBS data volume (prevent_destroy) with daily DLM snapshots; survives instance replacement - postgres password via random_password -> SSM SecureString, fetched at boot by instance profile (never in user-data); optional secrets (operator key, Pinata JWT, S3 secret) follow the same path - SSM Session Manager access (no SSH by default), IMDSv2 required, metrics port closed unless explicitly opened - SSM command document for image upgrades (user-data runs once) Verified: terraform fmt/init/validate clean; rendered compose passes docker compose config; rendered user-data passes bash -n. Co-Authored-By: OpenClaude <[email protected]> * fix(infra): address CodeRabbit review feedback - api_url output now honors the public_url override (local.public_url) - disable xtrace around SSM secret fetch + .env write so secret values never reach the bootstrap log - add retry/timeout flags to the docker compose download - validate snapshot_retain_count (1-1000) at plan time - add language tags to fenced tree blocks (MD040) Co-Authored-By: OpenClaude <[email protected]> * fix(infra): address PR #27 review feedback - security group now derives its VPC from the selected subnet, so subnet_id overrides into non-default VPCs work - s3_access_key_id moves to SSM SecureString (same path as the secret key) instead of being embedded in user-data and state - ignore user_data drift on the instance: it only runs at first boot, so re-rendering caused pointless stop/starts; README documents the -replace workflow for config changes - DLM snapshot targeting is stack-specific (Snapshot + Name tags), not any Snapshot=true volume in the account - comment in fly.toml that it targets the shared test instance - user-data compose up gains --remove-orphans (consistent with the upgrade SSM document) - node service gets an HTTP /health healthcheck in compose Co-Authored-By: OpenClaude <[email protected]> * feat(infra): optional customer-managed KMS key for SSM secrets Add ssm_kms_key_id (default null = AWS-managed aws/ssm key). When set, all five SecureString parameters are encrypted with the CMK and the instance role gains kms:Decrypt scoped to that key — without it the boot-time secret fetch would fail. Co-Authored-By: OpenClaude <[email protected]> * fix(infra): scope CMK kms:Decrypt to this stack's SSM parameters The grant on the customer-managed key was unconditioned, allowing the instance role to decrypt any ciphertext under the same CMK. Restrict it with kms:ViaService = ssm.<region>.amazonaws.com and kms:EncryptionContext:PARAMETER_ARN limited to the stack's parameter ARNs. Co-Authored-By: OpenClaude <[email protected]> --------- Co-authored-by: OpenClaude <[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]> |