proto/holger-doors.proto
syntax = "proto3";
// `holger.doors.v1` (lane B3, 2026-10-26): the acts that had no gRPC door, so the
// thin client `holger-cli` could not do them. Its own package so `holger.proto`, and
// every copy of it a console vendors, does not move.
//
// * `BundleService.Export`: the signed airgap export of the approved set, the same
// candidate `holger export` writes, streamed to the caller. Admin only, audited first
// (no record, no export).
// * `FirstKeyService.Invite`: the link that enrols the box's FIRST admin key (a
// passkey), for a box nobody can open a browser on yet. Admin only (the box's own
// bootstrap credential), audited, and only while no admin key exists.
package holger.doors.v1;
service BundleService {
// The candidate's members, in order: each member's bytes in one or more chunks that
// name its path; the LAST chunk carries the summary and no data.
rpc Export(ExportBundleRequest) returns (stream ExportBundleChunk);
// The admin's authorization of a FIRST import (lanes HOL-CDS, HOL-NCSC): this vendor,
// product and sequence may set the anti-rollback floor once. Admin only, audited, used
// once; the same act as `holger bundle allow-first-import` and
// `POST /-/bundle/first-import`.
rpc AllowFirstImport(AllowFirstImportRequest) returns (AllowFirstImportReply);
}
message AllowFirstImportRequest {
// The bundle's vendor (its `supplier`).
string vendor = 1;
string product = 2;
// The sequence that establishes the floor.
uint64 sequence = 3;
}
message AllowFirstImportReply {
string vendor = 1;
string product = 2;
uint64 sequence = 3;
// Who authorized it.
string by = 4;
// Unix seconds.
int64 at = 5;
}
message ExportBundleRequest {
string bundle_id = 1;
// Monotonic per (supplier, product): the secure side's anti-rollback floor.
uint64 sequence = 2;
string supplier = 3;
string product = 4;
string release_version = 5;
// 0 is the default of 30 days.
uint32 expires_days = 6;
// Only these artifacts (file name, name or `name@version`). Empty: every AVAILABLE
// artifact in production.
repeated string artifacts = 7;
}
message ExportBundleChunk {
// The member these bytes belong to (relative to the candidate directory).
string path = 1;
bytes data = 2;
// Set on the last chunk only.
ExportBundleSummary summary = 3;
}
message ExportBundleSummary {
string bundle_id = 1;
uint64 sequence = 2;
// Computed from the stored evidence, never typed in.
string verdict = 3;
string candidate_sha256 = 4;
// One line per exported artifact, as `holger export` prints it.
repeated string artifacts = 5;
uint32 members = 6;
}
service FirstKeyService {
// A single-use invite that enrols the first admin key. Refused once an admin key exists.
rpc Invite(FirstKeyRequest) returns (FirstKeyReply);
}
message FirstKeyRequest {
// The name the first admin is enrolled under. Empty: theirs to choose at enrolment.
string display = 1;
}
message FirstKeyReply {
// The invite secret: shown ONCE, never stored (only its hash is).
string invite = 1;
string id = 2;
int64 expires = 3;
// The groups the redeemed key holds.
repeated string groups = 4;
// Where to redeem it, relative to the console's origin (`/login?invite=<secret>`).
string path = 5;
string words = 6;
}
// `DownstreamService.Invite` (lane HOL-PRODUCTS-7, 2026-10-30): the signed invite a
// downstream of this server is handed (`holger-cli downstream invite`, the console's
// Invite button). A `via: Front` downstream gets a direct invite (schema v1: the server's
// sync door, no tunnel); a `via: Wireguard` one gets the estate's (schema v2, the link's
// pre-shared key inside the signed bytes, the front's interface). Signed with the server's
// sync key, for the downstream's key; admin only, audited.
service DownstreamService {
rpc Invite(DownstreamInviteRequest) returns (DownstreamInviteReply);
}
message DownstreamInviteRequest {
// The enrolled downstream's name.
string name = 1;
// A direct invite's door (`http(s)://host:port`). Empty: the server's own sync door.
string door = 2;
// A WireGuard invite's front key and endpoint. Empty: the front's own, as it offered them.
string hub_key = 3;
string endpoint = 4;
}
message DownstreamInviteReply {
// The signed invite (JSON): the file the downstream's RON names in `sync.upstreams[].invite`.
bytes invite = 1;
string schema = 2;
// The door the downstream reaches this server at.
string door = 3;
// A WireGuard downstream's address on the front's interface; empty for a direct one.
string address = 4;
// This server's sync name and key: what the downstream's RON trusts.
string upstream = 5;
string key = 6;
}