pub struct Node { /* private fields */ }Expand description
A running node daemon serving repos under a root directory.
Implementations§
Source§impl Node
impl Node
Sourcepub fn bind(root: &Path, port: u16) -> Result<Self>
pub fn bind(root: &Path, port: u16) -> Result<Self>
Binds to 127.0.0.1:port (0 = ephemeral) over root, with no
authentication — localhost/dev only.
§Errors
Returns an error when the socket cannot be bound or root cannot be
created.
Sourcepub fn bind_with_auth(
root: &Path,
port: u16,
auth: Option<AuthTable>,
) -> Result<Self>
pub fn bind_with_auth( root: &Path, port: u16, auth: Option<AuthTable>, ) -> Result<Self>
Binds like Node::bind; when auth is Some, every request
must carry valid basic-auth credentials from the table or it is
answered with 401 before touching git.
§Errors
Same failure modes as Node::bind.
Sourcepub fn bind_full(
root: &Path,
addr: &str,
port: u16,
auth: Option<AuthTable>,
tls: Option<(Vec<u8>, Vec<u8>)>,
) -> Result<Self>
pub fn bind_full( root: &Path, addr: &str, port: u16, auth: Option<AuthTable>, tls: Option<(Vec<u8>, Vec<u8>)>, ) -> Result<Self>
Full-control bind: address, port, auth, and optional TLS (PEM certificate chain + PEM private key).
A non-loopback addr is refused without TLS — plaintext basic
auth must never cross a real network (standing privacy rule:
nothing leaves loopback without an explicit, protected choice).
§Errors
Returns an error when the socket cannot be bound, root cannot
be created, the TLS material is invalid, or a non-loopback bind
is requested without TLS.
Sourcepub fn enable_api_body_limit(&mut self, bytes: NonZeroU64)
pub fn enable_api_body_limit(&mut self, bytes: NonZeroU64)
Sets the absolute body ceiling for every /api/... route.
Sourcepub fn disable_browser_writes(&mut self)
pub fn disable_browser_writes(&mut self)
Removes browser mutation controls and their preparation endpoint.
It withholds authorship, not credentials (D73). A browser under
this flag renders no control that would put an operation in the op
log – no verdict, no comment, no /api/prepare – because that is
the launch gate D39 shipped behind and the thing an operator
switches on when they are ready for it.
It does not withhold signing in, enrolling the passkey that signs
in, asking for access (D72), or the operator’s console. None of
those reaches the log: the accounts store is a node-owned file and
revocation is deletion (D36). Withholding them was the same flag
doing two jobs, and the second job made the node’s own manifest
untrue – passkeys=enabled beside a posture that would not serve
the file the ceremony is written in.
Sourcepub fn serve_single_repository(&mut self, repo: &str) -> Result<()>
pub fn serve_single_repository(&mut self, repo: &str) -> Result<()>
Presents one repository as this node’s entire browser surface.
/ becomes that repository instead of the index, and the browser
answers for no other repository. This is what a node serving a
project’s own domain wants: a reader arriving at
git.example.com came for that project, and an index naming
every other repository the host holds is both noise and a
disclosure.
Presentation only. It changes no grant: git access stays the ACL’s answer, and a repository hidden here is still clonable by whoever could clone it before.
§Errors
Refuses a name this node could not hold, checked here rather than at the call site: the router trusts this value against the disk, so the grammar has to be enforced where it is stored.
Sourcepub fn enable_ready_min_free_bytes(&mut self, bytes: u64)
pub fn enable_ready_min_free_bytes(&mut self, bytes: u64)
Sets the free-space floor below which /readyz refuses traffic.
Sourcepub fn enable_request_log(
&mut self,
path: PathBuf,
max_bytes: u64,
) -> Result<()>
pub fn enable_request_log( &mut self, path: PathBuf, max_bytes: u64, ) -> Result<()>
Records every served request to path (D33), rotating it at
max_bytes — see limits::RequestLog for the line format, the
rotation rule, and what is deliberately never written.
§Errors
Returns the failure to open the file. Fatal by design: an operator who asked for a record and did not get one should learn that at startup rather than from its absence during an incident.
Sourcepub fn enable_rate_limit(
&mut self,
api_per_minute: Option<NonZeroU32>,
git_per_minute: Option<NonZeroU32>,
)
pub fn enable_rate_limit( &mut self, api_per_minute: Option<NonZeroU32>, git_per_minute: Option<NonZeroU32>, )
Limits each authenticated user to the given requests per minute
per class (D33). None leaves that class unlimited.
Never applied to the loopback hook callback, to a holder of a D29
@node grant, or on a node without authentication — see
Node::serve_forever for why each of those would be worse than
the flood it prevents.
Sourcepub fn enable_quotas(
&mut self,
push_bytes: Option<NonZeroU64>,
workspaces: Option<NonZeroU32>,
)
pub fn enable_quotas( &mut self, push_bytes: Option<NonZeroU64>, workspaces: Option<NonZeroU32>, )
Sets the per-user quotas (D37): the largest git request body one
user may send, and the most workspaces one user may hold at once.
None leaves that ceiling off.
Exempt exactly where the D33 rate limiter is exempt, and for the
same reason — see Node::serve_forever. A quota that can lock
an operator out of their own node is the failure this must not
cause.
Sourcepub fn enable_platform(&mut self, platform: Platform)
pub fn enable_platform(&mut self, platform: Platform)
Enables the platform API (/api/submit, /api/view) backed by
platform. Call before Node::serve_forever.
Sourcepub fn enable_queue(&mut self, config: QueueConfig)
pub fn enable_queue(&mut self, config: QueueConfig)
Enables POST /api/queue/run (D5, D68).
Without it the endpoint answers 501. There is deliberately no timer: a node that spends CI on its own schedule surprises whoever pays for it, and a round only a clock can start is one no test can reach without waiting on wall-clock time. An operator’s cron, a hook, or a person decides the cadence.
Sourcepub fn platform(&self) -> Option<&Platform>
pub fn platform(&self) -> Option<&Platform>
The platform this node serves, or None if the platform API was
never enabled.
Sourcepub fn reconcile_refs(&self) -> RefReconciliation
pub fn reconcile_refs(&self) -> RefReconciliation
Brings the bare repos back into agreement with the view before the
node serves anything. See Platform::reconcile_git_refs for what
it repairs and what it refuses to.
Separate from Node::enable_platform and from
Node::serve_forever so it is called deliberately: it writes git
refs and can append compensating ops, which is not something a
constructor should do behind a caller’s back.
Sourcepub fn watch_keys_file(&mut self, path: PathBuf)
pub fn watch_keys_file(&mut self, path: PathBuf)
Watches the trusted-keys file and regenerates
<root>/.choir/allowed_signers whenever its mtime moves, so
registering a signing key is “append a line” — the same
mechanism the platform registry already uses for submission
keys, which until now diverged from push-certificate
verification and left the two lists out of step.
Sourcepub fn watch_acl_file(&mut self, path: PathBuf) -> Result<(), String>
pub fn watch_acl_file(&mut self, path: PathBuf) -> Result<(), String>
Enforces per-repository authorization (D29) from path, reloaded
whenever its mtime moves — so granting access is “append a line”,
the same discipline as the trusted-keys file.
§Errors
Returns a message when the file cannot be read or does not parse. This is fatal by design: there is no previous table to fall back to at startup, and an empty table under a fail-closed ACL locks out everyone including the operator.
Sourcepub fn behind_tls_proxy(&mut self)
pub fn behind_tls_proxy(&mut self)
Declares that a TLS-terminating proxy sits in front of this node,
so absolute URLs it builds are written https and the cookies it
sets carry Secure.
Declared by the operator rather than read from
X-Forwarded-Proto, because the node cannot tell a header its
proxy set from one a client sent: trusting it would mean any
caller that can reach the node decides how its invite links are
spelled. The proxy this repository ships already sets that
header rather than appending to it, for the same reason
X-Forwarded-For is cleared there (D59), and a declaration needs
no such care.
The defect this exists for: an invite is a bearer credential
carried in a URL, and behind the proxy the node was writing that
URL with http. The recipient’s first request would carry the
credential in cleartext and only then be redirected.
Sourcepub fn enable_passkeys(&mut self)
pub fn enable_passkeys(&mut self)
Turns on passkeys: WebAuthn enrolment and the browser write path that verifies assertions against enrolled keys (D39, D71).
Separate from Node::enable_accounts even though both need the
accounts store, because a node can reasonably offer self-service
credentials without offering browser signing, and the private beta
says in its manifest that it does exactly that. Without this
switch that sentence could not be true: the enrolment routes and
platform’s assertion check are both reachable the moment the
store exists.
Sourcepub fn enable_accounts(
&mut self,
path: PathBuf,
keys_out: Option<SshKeysOut>,
actor_keys: Option<PathBuf>,
) -> Result<(), String>
pub fn enable_accounts( &mut self, path: PathBuf, keys_out: Option<SshKeysOut>, actor_keys: Option<PathBuf>, ) -> Result<(), String>
Turns on account and token self-service (D36) from the store at
path, optionally generating the authorized_keys D31’s forced
commands live in.
§Errors
Refuses without --auth-file and without --acl-file, for the
reason D29 and D33 refuse the same combinations: a credential
issued on a node that authenticates nobody is not a credential,
and one issued on a node with no ACL is a credential to every
repository, which is the thing being issued against. Also
returns the store’s own load failures.
actor_keys names the trusted-keys file a redemption may bind
one actor key into (--invite-binds-keys). None keeps the
pre-existing behaviour, in which an actor key reaches the node
only by an operator editing that file.
Sourcepub fn write_ssh_handoff(&self, path: &Path) -> Result<()>
pub fn write_ssh_handoff(&self, path: &Path) -> Result<()>
Writes the handoff file the SSH shim reads (D31): where this daemon is listening, and the loopback secret its git hooks authenticate with.
Both are per-process values — an ephemeral port, a secret minted at startup — so a forced command written once cannot carry them. The secret is not returned to the caller, only written, at 0600.
The ACL file goes in too, when one is configured, so a forced
command that forgot --acl-file still enforces what this daemon
enforces rather than reaching every repository.
§Errors
Any I/O error creating or writing the file.
Sourcepub fn create_repo(&self, name: &str) -> Result<()>
pub fn create_repo(&self, name: &str) -> Result<()>
Creates a bare repo name (e.g. "owner/repo.git") with pushes
enabled.
§Errors
Fails when the path exists or git init fails.
Sourcepub fn adopt_repo(&self, name: &str) -> Result<()>
pub fn adopt_repo(&self, name: &str) -> Result<()>
Brings an existing bare repo under this root back under the
sequencer: the same hook and the same git config
Node::create_repo installs.
This is what a restore needs, and without it a restore is silently
unsound. Objects arrive as a bundle, which git clone --bare and
git fetch both unpack into a repo with no pre-receive hook —
and a repo with no hook is served normally while every push into it
bypasses the sequencer entirely, landing refs that no op in the log
ever records. The alternative order is worse: letting the node
create the repos empty and unpacking afterwards means startup
reconciliation sees a log naming commits git does not have, decides
the log is unbackable, and appends retractions for every restored
ref (see Platform::reconcile_git_refs).
It also re-points gpg.ssh.allowedSignersFile, which
Node::create_repo wrote as an absolute path into the root that
existed then. A restore onto a different path leaves that config
naming a directory that may not exist, or worse, one that does and
holds someone else’s keys.
Idempotent, and safe to run on every start. The one value it does
not overwrite is receive.certNonceSeed: rotating it would refuse
the signed pushes already in flight against the old seed.
§Errors
Fails when the path does not exist, is not a git repository, or a
git config call fails.
Sourcepub fn adopt_existing_repos(&self) -> Result<usize>
pub fn adopt_existing_repos(&self) -> Result<usize>
Adopts every bare repository already under this node’s root, whatever put it there, and returns how many.
Adoption is what installs the pre-receive hook, so until a
repository has been adopted it is served with no hook and
every push into it bypasses the sequencer, landing refs that no
op in the log ever records — invariants 5 and 6 both, broken
silently.
That used to be reachable in ordinary operation, because
adoption only ever happened for repositories named in
--create. A repository restored from a bundle, moved in, or
created against a running node was listed nowhere and hooked
never. Walking the root closes the gap at its source: the
question “what is this node about to serve” is answered by the
filesystem, which is the same thing portable::export already
asks.
Idempotent, and meant to run on every start: the one value
Node::adopt_repo does not overwrite is
receive.certNonceSeed, so an ordinary restart costs a few git config calls and changes nothing.
§Errors
Fails when the root cannot be walked, or when a repository under it cannot be adopted. Both are fatal rather than skipped: serving an unadopted repository is the exact failure this prevents, so a node that cannot guarantee the hook must not start.
Source§impl Node
impl Node
Sourcepub fn serve_forever(&self)
pub fn serve_forever(&self)
Serves requests until the process exits. Run on a dedicated thread.
§Rate limiting and who is exempt (D33)
The check runs on the request’s own thread, after authentication — it has to be after, because the bucket is per authenticated user, and it must not be on the accept loop, which exists to accept.
Three exemptions, each because applying the limit would be worse than the flood it prevents:
- The loopback hook callback. A push of N refs makes N
/api/git-updatecalls; refusing the fourth one fails the push halfway and drives the retraction path for refs git will never create. It carries a node-minted secret over loopback, so it is not an untrusted caller in the first place. - A holder of a D29
@nodegrant. That grant is already total authority over the node. Throttling the one actor who can repair it, during the incident the limiter is reporting, is the lockout this feature must not cause. - Any node without
--auth-file. There is no per-user identity to bucket by, and a single sharedanonbucket is a self-inflicted denial of service rather than a limit.
The D37 quotas (Node::enable_quotas) take the same three,
from the same computed value rather than a second copy of the
rule: whether a request is metered is one question, and answering
it twice is how the two answers start to differ.