Skip to content

feat(parser_http_server): HTTP+JSON pivot for TVC public ingress - #450

Open
pepe-anchor wants to merge 10 commits into
mainfrom
pepefigueira/prs-581-03-pivot-v1
Open

feat(parser_http_server): HTTP+JSON pivot for TVC public ingress#450
pepe-anchor wants to merge 10 commits into
mainfrom
pepefigueira/prs-581-03-pivot-v1

Conversation

@pepe-anchor

@pepe-anchor pepe-anchor commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Why

Turnkey's TVC public ingress is HTTP only: Cloudflare in front of app-<uuid>.turnkey.cloud rejects gRPC with 403 (verified 2026-05-16). So switching the parse path onto the pivot needs a binary that speaks HTTP and JSON natively and calls parser_app::routes::parse in process, instead of gRPC over vsock.

This is the keystone of the PRS-581 stack. Two PRs are built directly on it in parallel (in-enclave X-Stamp auth, real NSM boot proof), so as much as it is a feature, it is the set of extension points those need.

What

Open v1 and v2 routes only, plus /health. No payment enforcement, no auth, no proto change in this PR.

The three seams it owns, and why each is shaped that way:

  1. Handlers take raw Bytes, never Json<T>. An X-Stamp signature covers the exact request bytes; letting serde deserialize and re-serialize changes them (key order, whitespace) and invalidates every signature. parse_envelope(&[u8]) deserializes while the caller keeps the untouched slice. A test pins this so a future refactor cannot quietly undo it.
  2. BootProofSource trait with a StaticBootProof implementation, so the NSM-backed one is a new file rather than a rewrite. awsAttestationDocB64 is an empty string here: empty, never faked, because a strict verifier must reject an unattested response outright.
  3. Manifest fields are borsh, not the JSON on disk. /qos.manifest holds JSON at qos rev 365ba7ed, but qosManifestB64 and qosManifestEnvelopeB64 are borsh bytes: the Go verifier borsh-deserializes both, and the attestation doc's user_data is sha256(borsh(manifest)). Base64-ing the file bytes would produce fields nothing can verify. Verified against visualsign-turnkeyclient manifest/parser.go.

Also threads include_intermediate_output through for parity with the gateway's REST shape (#414). Every response, success and error, carries bootProof.

The branch also carries the two stagex commits from PRS-581 PR 02, so the pivot has an image to deploy: the matrix builds parser_http_server and parser_grpc_server, the root Makefile gains the rules both legs need, images/parser_grpc_server/Containerfile is added, and the concurrent release-note writes gain a best-effort retry so the legs mostly stop clobbering each other's deployment blocks (see the correction below, the mechanism is not a lock).

Rebase onto main after #446

#446 merged, so this branch is rebased onto current main and the merge commit is flattened away. Three commits now, no merge.

The rebase itself was textually clean: this branch never edited host_primitives/src/turnkey.rs or parser/gateway/src/main.rs, it only imports from them.

What it did leave behind was a semantic mismatch. The pivot was written against host_primitives::turnkey as of the fork point, which had error_response but not yet success_response (that landed in 87816dff, after the fork). So after the rebase handle_parse was still assembling the success wrapper field by field, which is exactly the second copy of the wire contract #446 existed to delete. Fixed in its own commit: the success path now goes through success_response, matching what parser_gateway does.

No wire change. success_response fills the same three fields and sets error: None, so the emitted JSON is byte-identical, and bootProof stays camelCase with exactly its six camelCase keys. Three separate tests pin that and all still pass (host_primitives::turnkey::boot_proof_wire_shape_is_exactly_six_camel_case_keys, parser_http_server's static_boot_proof_has_the_six_keys_and_a_real_ephemeral_pubkey, and the http_server integration test's key-set assertion on both the success and the 400 response).

Review pass, four more commits

A pre-push review round on top of the rebase. Nothing here changes the wire
shape or the three seams the stacked PRs plug into.

Every response now really does carry bootProof. The PR claimed that and
was wrong for three statuses. axum rejects an oversized body, an unmatched
route and a disallowed method before any handler runs, so 413, 404 and 405
went out as bare axum errors with no envelope. 413 is fixed with a middleware,
since DefaultBodyLimit rejects while reading the body and no handler sees it.
404 and 405 deliberately do not share that middleware: handle_parse
legitimately returns 404 itself for Code::NotFound with the parser's real
message, and a middleware keyed on status alone cannot tell that apart from an
unmatched route, so it would replace a genuine parse failure with "not found".
Those two go through Router::fallback and method_not_allowed_fallback,
which axum invokes only when no handler ran. The integration test asserts the
envelope on all four error paths now.

The parse call no longer blocks the reactor. parse is CPU bound, so
running it on the async task pinned a Tokio worker per concurrent request and
would starve the health check Turnkey polls on a 1 to 2 vCPU replica.
block_in_place matches what parser_app's Processor already does around
this same call on the vsock path.

Error messages stopped reflecting request bodies. serde_json's Display
for a type mismatch embeds the offending value, so a malformed request echoed
up to the full body back to an unauthenticated caller. The client gets a fixed
message; the detail goes to stderr, along with new logging on three internal
error paths that previously failed silently.

The TODO(#231) clippy exemptions are gone, not deferred: the ephemeral
key load propagates its error and SIGTERM registration falls back to ctrl-c,
so the crate-level unwrap/expect/panic allows could be deleted outright.

NEAR is compiled in. The default feature set said it mirrored
parser_app's and did not, so a NEAR request would have succeeded against
parser_app and failed against the pivot. That was a silent divergence, not a
deliberate trim.

Two correctness fixes to the notes this branch generates. The release notes
told an operator that /v2 is TVC-enforced when GATEWAY_SIGNING_PUBKEY_HEX
is set and that parser_gateway performs x402 verify and settle. Neither is
true yet: that variable is read nowhere in src, /v2 routes to the same open
handler as /v1, and there is no payment code in the tree. Both notes now
describe the scaffolding as scaffolding.

Correction to ac9e5a47's commit title. It says "serialize release-note
writes" and the mechanism does not serialize. The retry plus read-back is best
effort: it catches this leg's own write being clobbered before it reads back,
but a later leg writing from a stale read can still land after that read-back
succeeded and drop this leg's block. The in-file comment now says so plainly.
Blast radius is a cosmetic release body, not the build or deploy path, and a
manual re-run restores it. Left as best effort rather than adding If-Match or
per-target release assets, which is a real design change and out of scope here.
Not amending the commit message, since this branch has stacked children.

Also pins the borsh encoding of qosManifestB64 and qosManifestEnvelopeB64
with a round-trip test. Nothing asserted it, so swapping to the JSON that
actually sits on disk would have broken Go-side verification with every test
still green.

Test evidence

make -C src test  -> 1614 passed, 0 failed (all suites)
     host_primitives                    5 passed
     parser_http_server                 4 passed
     parser_gateway                    11 passed
     integration/http_server            1 passed
cargo fmt --all -- --check              -> clean
cargo clippy --all-targets -D warnings  -> clean
cargo build -p parser_http_server --features vsock -> compiles (production config)

cargo clippy -p parser_http_server --features vsock --all-targets -D warnings -> clean
cargo test   -p parser_http_server --features vsock                          -> clean

Those last two mattered: they were newly broken and are now fixed. A
qos_core dev-dependency added for the borsh round-trip test pulled in the
mock feature, which cannot be unified in one build graph with this crate's
vsock feature (qos_core's own compile_error! forbids vm plus mock).
Plain cargo build --features vsock still worked and CI never passes vsock,
so nothing was red, but the exact check this PR's acceptance criteria invite
would have failed with a confusing error pointing at qos_core. Fixed at the
source by building the test's ManifestEnvelope from explicit field literals
so the dev-dependency could be deleted.

The http_server integration test covers: health 200; a v1 parse whose
signature.publicKey matches the generated ephemeral key; bootProof with
exactly the six production keys; v2 identical to v1; a malformed body
returning 400; an unmatched route returning 404; a disallowed method returning
405; and a 65 KiB body returning 413. All four error paths assert the six
bootProof keys.

It fails fast if the server dies before binding, polling try_wait alongside
the port. wait_until_port_is_bound loops forever, so without that check a
pivot built for the enclave (which looks for the absolute in-enclave key path
and exits at startup) would hang CI instead of failing it. The client now also
carries a request timeout, so a server that accepts a connection and never
answers fails rather than hanging. Its generated key workdir is gitignored, so
a P256 private key cannot linger on a shared runner.

Rollback

Revert the commits. This adds a new binary that nothing calls yet and changes no existing behavior, so a revert is inert. Nothing is deployed by merging it.

Linear

PRS-581

Stacked on #446 (merged). Supersedes the pivot portion of #304.

🤖 Generated with Claude Code

@pepe-anchor pepe-anchor added the CI label Aug 6, 2026
pepe-anchor added a commit that referenced this pull request Aug 26, 2026
…446)

## Why

Two host binaries have to serve the same Turnkey JSON envelope to the
same client.

- `parser_gateway`: REST in front, gRPC to `parser_grpc_server` behind.
Non-TEE local dev and CI, so it emits a mock `bootProof`.
- `parser_http_server` (#450): HTTP+JSON inside the enclave, calling
`parser_app` in process, no gRPC hop. This is what gets deployed to the
TVC, because Cloudflare in front of `app-<uuid>.turnkey.cloud` rejects
gRPC with 403. It emits a real attested `bootProof`.

Different transports, different trust levels, one wire contract. The Go
[`visualsign-turnkeyclient`](https://github.com/anchorageoss/visualsign-turnkeyclient)
and the wallet integrators behind it cannot tell the two apart, and must
not be able to.

Two features also land in that envelope during PRS-581: `bootProof`
(#337) and `intermediateOutput` (#414). If the definition stays inline
in `parser_gateway`, the pivot copies it, and every envelope change from
here on gets made twice and kept in sync by hand. The unmerged x402
branch had already grown its own third copy.

So: one home, two importers, and the `bootProof` difference becomes a
parameter instead of a fork.

Downstream, #450, #451 and #452 build on `parser_http_server`; #449
builds on `parser_gateway`. Both sides import the envelope from here,
which is also what shrinks the PR #304 rebase from "reconcile two
envelope definitions" to "add a module".

## What

- `host_primitives::turnkey` becomes the single home for the Turnkey
request/response envelope, as the union of both existing definitions.
- `bootProof` is now an injected value rather than a hardcoded mock:
`error_response(msg, boot_proof)`. `parser_gateway` keeps its stable
local-dev mock through a local shim; the enclave pivot supplies a real
attested one.
- Construction moved with the types: `success_response` sits next to
`error_response`, so `parse_handler` no longer assembles the success
envelope field by field in the gateway.
- `parser_gateway` imports the shared types and keeps the four
`MOCK_BOOT_PROOF_*` constants and the tests that are about its own mock
and top-level wire shape. Three tests that would have duplicated
coverage now living in `host_primitives::turnkey` moved there instead of
being maintained in both places.
- `intermediate_output` gained `serde(default)` alongside its existing
`skip_serializing_if`. Without it the omitted key serialized fine but
failed to deserialize, so a response could be written and not read back.
Nothing in tree deserializes a response today (both binaries only emit
one), which is why no test caught it. The client-direction derives are
there so the envelope is symmetric for the out-of-tree clients that do
read it, and the new round-trip test is what keeps it that way. Happy to
drop those derives and the `default` until something in tree needs them,
if you'd rather not carry them.

No wire change. The gateway's JSON output is byte-identical.

## Test evidence

```
cargo test -p host_primitives -p parser_gateway
  host_primitives: 5 passed
  parser_gateway: 11 passed
cargo fmt --all -- --check: clean
make -C src lint: clean
```

The gateway test count moves from 13 to 11 because three tests relocated
to `host_primitives::turnkey`, where the types they cover now live: the
six-key `bootProof` assertion
(`boot_proof_wire_shape_is_exactly_six_camel_case_keys`), the empty
`intermediateOutput` omission check, and the Solana chain-metadata
discriminator test. `host_primitives` goes 4 to 5 with the new
round-trip test. No coverage was dropped.

The two regression tests that matter both still pass with unchanged
constant values: `mock_boot_proof_matches_production_wire_shape`,
`error_response_carries_mock_boot_proof`.

The "no wire change" claim was checked rather than assumed: a throwaway
test reconstructed the pre-refactor struct definitions verbatim from
base and byte-compared `serde_json` output against the new
`host_primitives::turnkey` types across success, error,
signature-present, and intermediate-output-present cases. All identical.

One trap worth recording: the x402 branch's `TurnkeyResponseWrapper` had
no `rename_all = "camelCase"`, which the gateway's inline struct did
have. Adding `boot_proof` without it would have serialized as
`boot_proof` instead of `bootProof` and silently broken the wallet
contract. The six-key wire-shape test catches it.

## Rollback

Revert the commits. No deploy, no migration, no wire change, so a revert
restores the previous state exactly.

## Linear

PRS-581

Stack position: base of the PRS-581 stack. #337, #414.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Base automatically changed from pepefigueira/prs-581-01-turnkey-envelope to main August 26, 2026 15:46
pepe-anchor and others added 4 commits August 26, 2026 15:58
Turnkey's TVC ingress is HTTP only (Cloudflare in front of
app-<uuid>.turnkey.cloud rejects gRPC with 403), so switching the parse
path onto the pivot needs a binary that speaks HTTP+JSON natively and
calls parser_app::routes::parse in-process.

Open v1 and v2 routes only. No payment enforcement, no auth, no proto
change: those are separate PRs on top. What this PR does own is the three
seams they plug into, so they can be written in parallel without
colliding: handlers take raw Bytes (an X-Stamp signature covers the exact
request bytes, and a Json<T> round-trip would invalidate it), bootProof
comes from a BootProofSource trait, and the manifest fields are already
borsh-encoded the way the Go verifier reads them.

The integration test fails fast if the server dies before binding. It
polls try_wait alongside the port, because wait_until_port_is_bound loops
forever: a pivot built with --features vsock looks for the absolute
in-enclave key path, exits at startup, and would otherwise hang CI
instead of failing it.

Co-Authored-By: Claude <noreply@anthropic.com>
The TVC deployment-details step hardcoded parser_app, so the only binary
that ever got a reproducible digest and a paste-ready deploy block was
parser_app. The pivot we are switching to (parser_http_server) needs the
same treatment before it can be deployed or probed.

Generalize the extraction over the matrix target and add the two missing
images. QOS version stays at 0.12.0 here; the bump is PRS-581 PR 7.

Co-Authored-By: Claude <noreply@anthropic.com>
The matrix gained parser_grpc_server and parser_http_server, but the root
Makefile had no rule for either, so both legs died at the Build step before
Docker ran. images/parser_grpc_server/Containerfile was missing entirely.

parser_grpc_server mirrors parser_gateway rather than parser_app: the crate
has no [features] table and is not a TVC pivot, so vsock/CHAIN_FEATURES are
deliberately omitted. Chains still link in through parser_app's default
features, since --no-default-features applies to the package being built,
not to its path dependencies.

Also fixes a lost update on the release body. Three legs now run the
deployment-details step concurrently, each doing gh release view then
gh release edit with a full-body overwrite. Per-target sentinels keep a
leg's own block idempotent across re-runs but do not serialize legs, so
whichever edit landed last dropped the others' sections. Retry with a
fresh read and verify the write stuck.

Co-Authored-By: Claude <noreply@anthropic.com>
…esponse

The pivot was written against host_primitives::turnkey as of the fork point,
before #446 grew a shared success_response helper. Rebasing onto main left
handle_parse assembling the wrapper field by field, which is the second copy of
the wire contract that #446 existed to remove.

No wire change: success_response fills the same three fields and sets
error: None, so the emitted JSON is byte-identical. The bootProof six-key
assertions in host_primitives, parser_http_server and the http_server
integration test all still pass.

Co-Authored-By: Claude <noreply@anthropic.com>
@pepe-anchor
pepe-anchor force-pushed the pepefigueira/prs-581-03-pivot-v1 branch from 7fe0208 to 7bc3e65 Compare August 26, 2026 16:20
@pepe-anchor
pepe-anchor requested a balanced review from Copilot August 26, 2026 16:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds an in-process HTTP/JSON ingress for Turnkey TVC, including boot-proof extension points and deployment packaging.

Changes:

  • Adds open v1/v2 parse routes and health endpoint.
  • Adds static boot-proof generation and HTTP integration coverage.
  • Extends container builds and stagex release metadata.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/parser/http-server/src/main.rs Implements HTTP ingress and parsing.
src/parser/http-server/src/boot_proof.rs Adds boot-proof abstraction and manifest encoding.
src/parser/http-server/Cargo.toml Configures server dependencies and features.
src/parser/http-server/build.rs Injects build version metadata.
src/integration/tests/http_server.rs Tests HTTP routes and response shape.
src/integration/Cargo.toml Adds HTTP test dependency.
src/Cargo.toml Registers the new workspace crate.
src/Cargo.lock Locks new crate dependencies.
Makefile Adds server image targets.
images/parser_http_server/Containerfile Packages the HTTP pivot.
images/parser_grpc_server/Containerfile Packages the gRPC server.
.github/workflows/stagex.yml Builds images and publishes deployment notes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/parser/http-server/Cargo.toml Outdated
Comment thread src/parser/http-server/src/main.rs
Comment thread src/parser/http-server/src/main.rs
Comment thread .github/workflows/stagex.yml Outdated
Comment thread .github/workflows/stagex.yml Outdated
Comment thread .github/workflows/stagex.yml
Comment thread src/parser/http-server/src/main.rs
pepe-anchor and others added 4 commits August 27, 2026 12:05
… reactor

Review feedback on the pivot, four separate defects in the HTTP surface.

axum rejects an oversized body, an unmatched route and a disallowed method
before any handler runs, so those responses went out as bare axum errors with
no bootProof at all. That contradicts the contract this PR states: every
response, success or error, carries one.

The 413 case goes through a middleware, because DefaultBodyLimit rejects while
reading the body and no handler ever sees it. 404 and 405 deliberately do not:
handle_parse legitimately returns 404 itself for Code::NotFound, carrying the
parser's real error message, and a middleware keyed on status alone sitting in
front of handler output cannot tell that apart from an unmatched route. It
would silently replace a real parse failure with "not found". Those two go
through Router::fallback and method_not_allowed_fallback, which axum invokes
only when no handler ran.

parse is CPU bound, not I/O bound. Running it directly on the async task pins
a Tokio worker thread per concurrent request and starves everything else on
that worker, including the health check Turnkey polls, on a 1 to 2 vCPU
replica. Wrapping it in block_in_place matches what parser_app's Processor
already does around this same call on the vsock path.

serde_json's Display for a type mismatch embeds the offending value, so the
old deserialize error reflected up to the full request body back to an
unauthenticated caller. The client now gets a fixed message and the detail
goes to stderr, alongside new logging on the three internal error paths that
previously failed silently.

The crate level clippy allows for unwrap, expect and panic were a TODO(#231)
placeholder. The sites they covered are gone rather than exempted: the
ephemeral key load propagates its error, and SIGTERM registration falls back
to ctrl-c instead of panicking.

The integration test grows a request timeout so a server that accepts a
connection but never answers fails instead of hanging, asserts the envelope on
the 400, 404, 405 and 413 paths, and its generated key workdir is now
gitignored so a P256 private key cannot linger on a shared runner.

Co-Authored-By: Claude <noreply@anthropic.com>
The pivot's default feature set was written to mirror parser_app's, and its
own comment says so, but it was missing near. A NEAR parse request would
therefore succeed against parser_app and fail against the pivot, which is a
silent behaviour difference between the two binaries rather than a deliberate
trim.

The Containerfile default moves with it, since that is where the image's
feature list is pinned and it carries the same "mirrors Cargo.toml" claim.

Chain specific deploys can still trim the set to shrink the image and the
attestation surface.

Co-Authored-By: Claude <noreply@anthropic.com>
The Go verifier borsh-deserializes both qosManifestB64 and
qosManifestEnvelopeB64, and the attestation doc's user_data is the sha256 of
the borsh manifest bytes. Nothing asserted that, so swapping the encoding to
the JSON that actually sits on disk, or introducing a field whose encoding is
not order stable, would break verification with every test still green.

Built field by field rather than through ManifestEnvelope's Default, which
only exists behind qos_core's mock feature. That feature cannot be unified in
one build graph with this crate's vsock feature, since qos_core's own
compile_error forbids vm and mock together, so pulling it in as a dev
dependency broke clippy and test runs configured for the production feature
set. Every field is a plain public value, so no derive is needed.

Also hoists the base64 engine import to the module top and folds the two
duplicated encode paths into one helper.

Co-Authored-By: Claude <noreply@anthropic.com>
… exist

The generated deploy notes told an operator that the v2 route is TVC-enforced
when GATEWAY_SIGNING_PUBKEY_HEX is set, and that parser_gateway performs x402
verify and settle. Neither is true yet. That variable is read nowhere in src,
v2 currently routes to the same open handler as v1, and there is no payment
code in the tree at all. Notes that overstate what is enforced are worse than
notes that say nothing, because the reader has no way to tell.

Both notes now describe the scaffolding as scaffolding and point at the
follow-up that implements it.

Also softens the comment on the release-note retry loop. It claimed the retry
plus read-back stops legs clobbering each other's block, but it is best effort
rather than a lock: it catches our own write being clobbered before we read it
back, while a later leg writing from a stale read can still land after our
read-back succeeded and drop our block. Blast radius is a cosmetic release
body, not the build or deploy path, and a manual re-run restores it.

Co-Authored-By: Claude <noreply@anthropic.com>
Generated by /finish P3 iteration 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 3 comments.

Suppressed comments (1)

src/parser/http-server/src/main.rs:355

  • This only exercises parse_envelope directly, so it still passes if either route changes its extractor from raw Bytes to Json<TurnkeyRequestWrapper>. The handler-level compile regression described in the resolved review thread is absent from the current diff; restore a test that calls parse_v1(State(state), Bytes::from_static(...)) (and/or routes a raw body through the router) so the X-Stamp extension seam is actually pinned.
    fn envelope_is_parsed_from_raw_bytes_not_reserialized() {
        // A later PR verifies an X-Stamp signature over the exact request
        // bytes. If a handler ever takes `Json<T>` and re-serializes, the
        // bytes change (key order, whitespace, unicode escaping) and every
        // stamp fails. Locking the seam here means that PR adds one call and

Comment thread src/parser/http-server/src/main.rs
Comment thread src/parser/http-server/src/main.rs Outdated
Comment thread src/parser/http-server/src/main.rs
Generated by /finish P3 iteration 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

src/parser/http-server/src/main.rs:135

  • Logging {e} still records attacker-controlled values from the unauthenticated request: as noted above, a serde type mismatch can include the offending string verbatim, allowing each 400 to write nearly 64 KiB of request data (including transaction contents) into enclave logs. Log only bounded metadata such as the error location/category while keeping the client message generic.
            eprintln!("invalid request body: {e}");

.github/workflows/stagex.yml:140

  • Including parser_gateway here generates the same paste-ready tvc deploy create instructions used for enclave pivots, even though the role note identifies it as host-side. Its image contains only the gateway, which defaults GRPC_ADDR to localhost and reports /health as unavailable without a separate gRPC backend (src/parser/gateway/src/main.rs:69-105,228-237), so following these generated instructions creates an unhealthy TVC deployment. Keep publishing its image, but restrict TVC deployment instructions to the actual pivot binaries (or give the gateway a separate non-TVC note).
        if: matrix.target.name == 'parser_app' || matrix.target.name == 'parser_gateway' || matrix.target.name == 'parser_http_server'

@pepe-anchor
pepe-anchor marked this pull request as ready for review August 27, 2026 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants