blob: f268143784ca2f05470207ecb3e678bc713d5c9d [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package cipd;
option go_package = "go.chromium.org/luci/cipd/api/cipd/v1;api";
import "google/protobuf/empty.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/timestamp.proto";
import "go.chromium.org/luci/cipd/api/cipd/v1/cas.proto";
// Manages CIPD packages and their associated metadata.
//
// A package is a named collection of one or more versions of a group of files.
// A package should typically be used for a single software component or
// dataset, not a conglomeration. A package instance is a concrete incarnation
// of a such version: it is a real file, and it is identified by its SHA256 (or
// SHA1 for older packages) digest, represented by ObjectRef proto in the
// protocol.
//
// A particular serialization of ObjectRef proto into a string is referred to
// as "instance ID" and it is used to identify package instances (mostly in
// the interface exposed by the CIPD command line client, but also in
// ResolveVersion RPC):
// * For legacy reasons SHA1 digests are encoded with lowercase hex encoding,
// which always results in 40 char ASCII strings).
// * Other digests are encoded using base64(digest + []byte{ref.HashAlgo}),
// where 'base64' is URL-safe and unpadded and '+' is concatenation.
// For SHA256 digests this results in 44 char ASCII strings that end with C.
//
// Instance files themselves are zip archives with some additional metadata.
// They are produced and consumed by cipd CLI client, and the backend mostly
// doesn't care about their internal structure.
//
// Package names look like rootless file system paths (e.g. "a/b/c"). The
// identifier of a package is always the full path. There's no notion of
// "relative paths", or "..", or any other similar constructs. The name of the
// last path component has no intrinsic significance either, though it is often
// used to denote a platform the package is intended for. For example
// "infra/tools/cipd/linux-amd64" package is intended for Linux running on amd64
// architecture. This is just a convention, and it is not enforced nor
// understood by the backend.
//
// Each element of the package namespace (known as "package path prefix" or just
// "prefix") can have some metadata attached to it. Currently it includes only
// an access control list (ACL) that specifies what roles are granted to what
// users for the given prefix and all packages underneath it.
//
// For example, granting READER role to user@example.com in ACL associated with
// prefix "a/b/c" gives user@example.com permission to read all package
// instances of "a/b/c" and any packages below it (e.g. "a/b/c/d"). See Role
// enum for list of roles.
//
// There's no finer permission granularity inside the package itself, e.g. it is
// not possible to allow to read some particular package instance, without
// allowing to read all instances of the package.
service Repository {
// Returns metadata associated with the given prefix.
//
// Requires the caller to have OWNER role for the requested prefix or any of
// parent prefixes, otherwise the call fails with PERMISSION_DENIED error.
//
// If the caller has OWNER permission in any of parent prefixes, but the
// requested prefix has no metadata associated with it, the call fails with
// NOT_FOUND error.
rpc GetPrefixMetadata(PrefixRequest) returns (PrefixMetadata);
// Returns metadata associated with the given prefix and all parent prefixes.
//
// Requires the caller to have OWNER role for the requested prefix or any of
// parent prefixes, otherwise the call fails with PERMISSION_DENIED error.
//
// Note that if the caller has permission to see the metadata for the
// requested prefix, they will also see metadata for all parent prefixes,
// since it is needed to assemble the final metadata for the prefix (it
// includes inherited properties from all parent prefixes).
rpc GetInheritedPrefixMetadata(PrefixRequest) returns (InheritedPrefixMetadata);
// Updates or creates metadata associated with the given prefix.
//
// Requires the caller to have OWNER role for the requested prefix or any of
// parent prefixes, otherwise the call fails with PERMISSION_DENIED error.
//
// This method checks 'fingerprint' field of the PrefixMetadata object. If the
// metadata for the given prefix already exists, and the fingerprint in the
// request doesn't match the current fingerprint, the request fails with
// FAILED_PRECONDITION error.
//
// If the metadata doesn't exist yet, its fingerprint is assumed to be empty
// string. So pass empty fingerprint when creating initial metadata objects.
//
// If the caller passes empty fingerprint, but the metadata already exists,
// the request fails with ALREADY_EXISTS error.
//
// Note that there's no way to delete metadata once it was created. Passing
// empty PrefixMetadata object is the best that can be done.
//
// On success returns PrefixMetadata object with the updated fingerprint.
rpc UpdatePrefixMetadata(PrefixMetadata) returns (PrefixMetadata);
// Returns a set of roles the caller has in the given prefix.
//
// Unlike GetPrefixMetadata call that requires OWNER access (since it returns
// a lot of detailed information), GetRolesInPrefix can be called by anyone.
//
// It understands and expands roles inheritance, e.g. if the caller is an
// OWNER, the result will also contain WRITER and READER (as they are implied
// by being an OWNER).
//
// Returns empty set of roles if the caller has no permissions to access the
// prefix at all or such prefix doesn't exist.
rpc GetRolesInPrefix(PrefixRequest) returns (RolesInPrefixResponse);
// Lists packages and subprefixes registered under the prefix.
//
// Lists either only direct descendants or recursively all descendants. The
// result is sorted lexicographically.
//
// For example, for packages ["a", "a/b", "a/c/d", "a/c/e/f"], listing of "a"
// will be:
// * {packages: ["a/b"], prefixes: ["a/c"]} if listing non-recursively.
// * {packages: ["a/b", "a/c/d", "a/c/e/f"], prefixes: ["a/c", "a/c/e"]} if
// listing recursively.
//
// Returns only packages and prefixes visible to the caller. This applies even
// when listing a prefix the caller has no direct read access to. For example,
// recursively listing the root prefix will return all packages the caller has
// read access to (no matter when in the hierarchy they are located), even if
// the caller has no READER permission in the root. It works as if the caller
// can freely browse the repository that contains only the packages they can
// see and nothing else.
rpc ListPrefix(ListPrefixRequest) returns (ListPrefixResponse);
// HidePackage marks the package as hidden.
//
// This removes it from the ListPrefix results, but doesn't otherwise affect
// its usage (e.g. it is still fetchable).
//
// Requires OWNER role for the package prefix. Returns PERMISSION_DENIED
// otherwise.
rpc HidePackage(PackageRequest) returns (google.protobuf.Empty);
// UnhidePackage marks the package as visible again.
//
// It's reverse of HidePackage.
//
// Requires OWNER role for the package prefix. Returns PERMISSION_DENIED
// otherwise.
rpc UnhidePackage(PackageRequest) returns (google.protobuf.Empty);
// DeletePackage removes the package (along with all its instances, tags, refs
// etc) from the repository.
//
// There's no undo. Once the package is deleted, it is gone forever.
//
// This operation has a potential to break various pinned historical CIPD
// ensure files and thus should not be used casually, only in extreme cases.
// Consider just hiding the package instead of deleting it.
//
// For the reasons above, the operation requires admin access: only owners of
// the repository root can delete packages.
//
// Returns:
// PERMISSION_DENIED if the caller is not a root owner.
// INVALID_ARGUMENT if the request is malformed.
// NOT_FOUND if the package doesn't exist.
rpc DeletePackage(PackageRequest) returns (google.protobuf.Empty);
// Registers a package instance in the repository (if it was uploaded to the
// storage already and wasn't registered yet) or initiates a new upload
// operation.
//
// Callers are expected to execute the following protocol:
// 1. Attempt to register a package instance by calling RegisterInstance.
// 2. On NOT_UPLOADED status, upload the package data and finalize the
// upload operation using Storage RPC service and upload_op from the
// response.
// 3. Once the upload operation is finalized, call RegisterInstance again,
// it should succeed with status REGISTERED now.
//
// If such instance is already registered by someone else, returns
// ALREADY_REGISTERED status. This is not an error.
//
// Callers must have roles WRITER or OWNER for the package prefix. Returns
// PERMISSION_DENIED otherwise.
rpc RegisterInstance(Instance) returns (RegisterInstanceResponse);
// Lists instances of a package, most recent first.
//
// Callers must have roles READER (or above) for the package prefix. Returns
// PERMISSION_DENIED otherwise.
//
// If the package doesn't exist, returns NOT_FOUND.
rpc ListInstances(ListInstancesRequest) returns (ListInstancesResponse);
// Returns package instances that have all given tags attached.
//
// Queries only instances of some particular package (i.e. this is not a
// global query).
//
// Callers must have roles READER (or above) for the package prefix. Returns
// PERMISSION_DENIED otherwise.
//
// If the package doesn't exist, returns NOT_FOUND.
rpc SearchInstances(SearchInstancesRequest) returns (SearchInstancesResponse);
// Creates a new ref or moves an existing one.
//
// A ref is a mutable named pointer to some existing package instance that
// can be used as a version identifier. For example, "latest" or "stable".
//
// Refs are namespaced to some particular package. E.g. "latest" ref in
// packages "A" and "B" are completely different entities not related to each
// other.
//
// Pointing a ref to an instance generally makes the instance "discoverable".
// For that reason the ref can be set only to instances that successfully
// passed all post-registration processing.
//
// Returns:
// PERMISSION_DENIED if the caller is not a WRITER for the prefix.
// INVALID_ARGUMENT if the request is malformed.
// NOT_FOUND if the package or the instance the ref points to doesn't exist.
// FAILED_PRECONDITION if the instance is still being processed.
// ABORTED if the instance has some failed processors associated with it,
// such instance is effectively broken and should not be used.
rpc CreateRef(Ref) returns (google.protobuf.Empty);
// Removes a ref.
//
// Not a failure if there's no such ref.
//
// Returns:
// PERMISSION_DENIED if the caller is not a WRITER for the prefix.
// INVALID_ARGUMENT if the request is malformed.
// NOT_FOUND if the package doesn't exist.
rpc DeleteRef(DeleteRefRequest) returns (google.protobuf.Empty);
// Lists refs defined in a package, most recently modified first.
//
// Returns:
// PERMISSION_DENIED if the caller is not a READER for the prefix.
// INVALID_ARGUMENT if the request is malformed.
// NOT_FOUND if the package doesn't exist.
rpc ListRefs(ListRefsRequest) returns (ListRefsResponse);
// Attaches one or more tags to an instance.
//
// Silently skips already attached tags.
//
// Tags are "key:value" pairs associated with a concrete package instance.
// They can be used for querying registered instances and for version
// resolution: if a tag is attached to one and only one instance of a package,
// the tag uniquely identifies this instance and such tag can be used as an
// alias of the instance ID.
//
// Tags generally should be assumed globally namespaced (e.g. it makes sense
// to query for all registered instances with a given tag, across all
// packages), and they don't have to be unique: same tag may be attached to
// multiple instances (of the same or different packages).
//
// Additionally, tags (unlike refs) are intended to be mostly static, since
// they usually relate to some properties of package instances, which are
// static entities. This is particularity important for tags used for version
// resolution.
//
// Attaching a tag to an instance generally makes the instance "discoverable".
// For that reason tags can be attached only to instances that successfully
// passed all post-registration processing.
//
// Returns:
// PERMISSION_DENIED if the caller is not a WRITER for the prefix.
// INVALID_ARGUMENT if the request is malformed.
// NOT_FOUND if the package or the instance doesn't exist.
// FAILED_PRECONDITION if the instance is still being processed.
// ABORTED if the instance has some failed processors associated with it,
// such instance is effectively broken and should not be used.
rpc AttachTags(AttachTagsRequest) returns (google.protobuf.Empty);
// Detaches one or more tags if they were attached.
//
// This call should not be used routinely, since tags are assumed to be
// static (and thus not detachable).
//
// It is occasionally useful for fixing screw ups though. For that reason,
// DetachTags is allowed only by OWNERS of a prefix (not WRITERS, like
// AttachTags).
//
// Returns:
// PERMISSION_DENIED if the caller is not an OWNER for the prefix.
// INVALID_ARGUMENT if the request is malformed.
// NOT_FOUND if the package or the instance doesn't exist.
rpc DetachTags(DetachTagsRequest) returns (google.protobuf.Empty);
// Takes a version string and resolves it into a concrete package instance.
//
// A version string can be any of:
// * A string-encoded instance ID, e.g. "abcdef....".
// * A ref name, e.g. "latest".
// * A tag, e.g. "version:1.10.3".
//
// Returns:
// PERMISSION_DENIED if the caller is not a READER for the prefix.
// INVALID_ARGUMENT if the request is malformed.
// NOT_FOUND if there's no such package or version.
// FAILED_PRECONDITION if the tag resolves to multiple instances.
rpc ResolveVersion(ResolveVersionRequest) returns (Instance);
// Produces a signed URL that can be used to fetch the package instance file.
//
// Returns:
// PERMISSION_DENIED if the caller is not a READER for the prefix.
// INVALID_ARGUMENT if the request is malformed.
// NOT_FOUND if there's no such instance.
rpc GetInstanceURL(GetInstanceURLRequest) returns (ObjectURL);
// Returns information about a package instance.
//
// Depending on fields set in the request, returns details such as when the
// instance was registered and by whom, refs pointing to it, tags attached to
// it, state of all processors that handled it (if any).
//
// May also be used as a simple instance presence check, if all describe_*
// fields in the request are false. If the request succeeds, then the
// instance exists.
//
// Returns:
// PERMISSION_DENIED if the caller is not a READER for the prefix.
// INVALID_ARGUMENT if the request is malformed.
// NOT_FOUND if the instance doesn't exist.
rpc DescribeInstance(DescribeInstanceRequest) returns (DescribeInstanceResponse);
// Returns information about a CIPD client package.
//
// Used by the client self-update procedure.
//
// Returns:
// NOT_FOUND if the package or the instance doesn't exist.
// FAILED_PRECONDITION if the instance is still being processed.
// ABORTED if the instance has some failed processors associated with it,
// such instance is effectively broken and should not be used.
rpc DescribeClient(DescribeClientRequest) returns (DescribeClientResponse);
}
////////////////////////////////////////////////////////////////////////////////
// Roles used in package prefix ACLs.
//
// A user can have one or more such roles for a package prefix. They get
// inherited by all subprefixes.
enum Role {
ROLE_UNSPECIFIED = 0;
// Readers can fetch package instances and package metadata (e.g. list of
// instances, all tags, all refs), but not prefix metadata (e.g. ACLs).
READER = 1;
// Writers can do everything that readers can, plus create new packages,
// upload package instances, attach tags, move refs.
WRITER = 2;
// Owners can do everything that writers can, plus read prefix metadata for
// all parent prefixes and all subprefixes, and modify prefix metadata for
// all subprefixes.
OWNER = 3;
}
message PrefixRequest {
// A prefix within the repository, e.g. "a/b/c".
string prefix = 1;
}
// PrefixMetadata is metadata defined at some concrete package prefix.
//
// It applies to this prefix and all subprefixes, recursively.
message PrefixMetadata {
message ACL {
// Role that this ACL describes.
Role role = 1;
// Users and groups that have the specified role.
//
// Each entry has a form "<kind>:<value>", e.g "group:..." or "user:...".
repeated string principals = 2;
}
// Prefix this metadata is defined at, e.g. "a/b/c".
//
// Note: there's no metadata at the root, so prefix must never be "".
string prefix = 1;
// An opaque string that identifies a particular version of this metadata.
//
// Used by UpdatePrefixMetadata to prevent an accidental overwrite of changes.
string fingerprint = 2;
// When the metadata was modified the last time.
//
// Managed by the server, ignored when passed to UpdatePrefixMetadata.
google.protobuf.Timestamp update_time = 3;
// Identity string of whoever modified the metadata the last time.
//
// Managed by the server, ignored when passed to UpdatePrefixMetadata.
string update_user = 4;
// ACLs that apply to this prefix and all subprefixes, as a mapping from
// a role to a list of users and groups that have it.
repeated ACL acls = 5;
}
message InheritedPrefixMetadata {
// Per-prefix metadata that applies to a prefix, ordered by prefix length.
//
// For example, when requesting metadata for prefix "a/b/c/d" the reply may
// contain entries for "a", "a/b", "a/b/c/d" (in that order, with "a/b/c"
// skipped in this example as not having any metadata attached).
repeated PrefixMetadata per_prefix_metadata = 1;
}
message RolesInPrefixResponse {
message RoleInPrefix {
Role role = 1;
}
// Unordered set or roles the caller has in the requested prefix.
repeated RoleInPrefix roles = 1;
}
////////////////////////////////////////////////////////////////////////////////
message ListPrefixRequest {
// A prefix within the repository to list, e.g. "a/b/c". Empty prefix is also
// accepted: it means "root of the repository".
string prefix = 1;
// If false, list only direct descendants of the prefix, otherwise all.
bool recursive = 2;
// If true, include hidden packages in the result.
bool include_hidden = 3;
}
message ListPrefixResponse {
// Lexicographically sorted list of full packages names.
repeated string packages = 1;
// Lexicographically sorted list of child prefixes (without trailing '/').
repeated string prefixes = 2;
}
////////////////////////////////////////////////////////////////////////////////
// PackageRequest names a package and nothing else.
message PackageRequest {
string package = 1;
}
////////////////////////////////////////////////////////////////////////////////
// RegistrationStatus is part of RegisterInstance RPC response.
enum RegistrationStatus {
REGISTRATION_STATUS_UNSPECIFIED = 0;
REGISTERED = 1; // the instance was successfully registered just now
ALREADY_REGISTERED = 2; // the instance already exists, this is OK
NOT_UPLOADED = 3; // the instance should be uploaded to Storage first
}
// Instance is a pointer to an instance of some package.
message Instance {
// A name of the package, e.g. "a/b/c/d".
string package = 1;
// A reference to the instance file in the storage.
ObjectRef instance = 2;
// User who registered the instance (output only).
string registered_by = 3;
// When the instance was registered (output only).
google.protobuf.Timestamp registered_ts = 4;
}
message RegisterInstanceResponse {
// Outcome of the operation, see the enum for possibilities.
//
// Defines what other fields are present.
RegistrationStatus status = 1;
// For statuses REGISTERED and ALREADY_REGISTERED contains details about the
// instance. Not set for NOT_UPLOADED status.
Instance instance = 2;
// For status NOT_UPLOADED contains a new upload operation that can be used
// together with Storage service to upload the instance file. Not set for
// other statuses.
UploadOperation upload_op = 3;
}
////////////////////////////////////////////////////////////////////////////////
message ListInstancesRequest {
// Name of a package to list instances of.
string package = 1;
// Number of instances to return on one page, default is 100.
int32 page_size = 20;
// Value of 'next_page_token' from the previous response.
//
// Can be used to resume fetching.
string page_token = 21;
}
message ListInstancesResponse {
// Package instances, sorted by registration time, most recent first.
repeated Instance instances = 1;
// Value to pass as 'page_token' in ListInstancesRequest to resume fetching or
// empty string if there's no more results.
string next_page_token = 20;
}
////////////////////////////////////////////////////////////////////////////////
message SearchInstancesRequest {
// Name of a package to query instances of (required).
string package = 1;
// Tags to look for (the found instances have ALL these tags attached).
//
// Due to internal limitations, the query is most efficient only when it
// specifies one tag to filter by. All additional tags are checked in a
// separate step after the initial query. For that reason when searching for
// multiple tags it is better to specify the most limiting tags first.
repeated Tag tags = 2;
// Number of instances to return on one page, default is 100.
int32 page_size = 20;
// Value of 'next_page_token' from the previous response.
//
// Can be used to resume fetching.
string page_token = 21;
}
message SearchInstancesResponse {
// Package instances, sorted by registration time, most recent first.
repeated Instance instances = 1;
// Value to pass as 'page_token' in SearchInstancesRequest to resume fetching
// or empty string if there's no more results.
string next_page_token = 20;
}
////////////////////////////////////////////////////////////////////////////////
// Ref is a mutable named pointer to some package instance that can be used
// as a version identifier.
message Ref {
// Name of the ref, e.g. "latest".
string name = 1;
// Name of the package where the ref is defined.
string package = 2;
// A package instance the ref is pointing to.
ObjectRef instance = 3;
// User who modified this ref the last time (output only).
string modified_by = 4;
// When the ref was modified the last time (output only).
google.protobuf.Timestamp modified_ts = 5;
}
message DeleteRefRequest {
// Name of the ref, e.g. "latest".
string name = 1;
// Name of the package where the ref is defined.
string package = 2;
}
message ListRefsRequest {
// Name of a package to list refs of.
string package = 1;
}
message ListRefsResponse {
// Package refs, sorted by modification time, most recently touched first.
repeated Ref refs = 1;
}
////////////////////////////////////////////////////////////////////////////////
// Tag is a key:value pair attached to some instance.
//
// Keys don't have to be unique, only the full pair should. For example,
// it is fine to have "version:1" and "version:2" tags attached to the same
// instance.
//
// The total length of the tag (as "key:value" pair) should be less that 400
// bytes.
message Tag {
// Key should be a lowercase identifier-like string ([a-z0-9_\-]+).
string key = 1;
// Value can be an arbitrary string.
string value = 2;
// User that attached this tag (output only).
string attached_by = 3;
// When the tag was attached (output only).
google.protobuf.Timestamp attached_ts = 4;
}
message AttachTagsRequest {
// The package that holds the instance we attach tags to.
string package = 1;
// The instance we attach tags to.
ObjectRef instance = 2;
// One or more tags to attach (order doesn't matter).
repeated Tag tags = 3;
}
message DetachTagsRequest {
// The package that holds the instance we detach tags from.
string package = 1;
// The instance we detach tags from.
ObjectRef instance = 2;
// One or more tags to detach (order doesn't matter).
repeated Tag tags = 3;
}
////////////////////////////////////////////////////////////////////////////////
message ResolveVersionRequest {
// The package that contains the instance we are resolving version of.
string package = 1;
// The version string to resolve, see ResolveVersion for details.
string version = 2;
}
////////////////////////////////////////////////////////////////////////////////
message GetInstanceURLRequest {
// The package that holds the instance we want to get URL of.
string package = 1;
// The instance we want to get URL of.
ObjectRef instance = 2;
}
////////////////////////////////////////////////////////////////////////////////
// Processor describes a state of some post-registration processing step
// performed on an instance.
message Processor {
enum State {
STATE_UNSPECIFIED = 0;
PENDING = 1;
SUCCEEDED = 2;
FAILED = 3;
}
// Internal identifier of the processor, e.g. "cipd_client_binary:v1"
string id = 1;
// The state of this processor, see the enum.
State state = 2;
// When the processor finished running (successfully or not).
google.protobuf.Timestamp finished_ts = 3;
// For SUCCEEDED state, result of the processing.
google.protobuf.Struct result = 4;
// For FAILED state, the error message.
string error = 5;
}
message DescribeInstanceRequest {
// The package that holds the instance we want to get the info for.
string package = 1;
// The instance we want to get the info for.
ObjectRef instance = 2;
// Whether the response should include "refs" field.
bool describe_refs = 3;
// Whether the response should include "tags" field.
bool describe_tags = 4;
// Whether the response should include "processors" field.
bool describe_processors = 5;
}
message DescribeInstanceResponse {
// The instance with all output fields filled in.
Instance instance = 1;
// Refs pointing to the instance, sorted by modification time, most recent
// first.
//
// Present only if the request has describe_refs == true.
repeated Ref refs = 2;
// Tags attached to the instance, sorted by the tag key first, and then
// by the timestamp (most recent first).
//
// Present only if the request has describe_tags == true.
repeated Tag tags = 3;
// State of the processors that handled the instance (if any), sorted by their
// ID.
//
// Present only if the request has describe_processors == true.
repeated Processor processors = 4;
}
////////////////////////////////////////////////////////////////////////////////
message DescribeClientRequest {
// The CIPD client package we want to get info about.
//
// For example 'infra/tools/cipd/linux-amd64'.
string package = 1;
// The client instance we want to get the info about.
ObjectRef instance = 2;
}
message DescribeClientResponse {
// The instance with all output fields filled in.
Instance instance = 1;
// Reference to the extracted client binary in the storage.
//
// The hash algo here always matches the algo used when uploading the client
// package file.
//
// Clients should expect to find an algo here that they do not support (if
// the server was updated to support a better algo). They should pick the best
// algo they support from client_ref_aliases list and use it for validation.
//
// Thus this field is mostly FYI.
ObjectRef client_ref = 2;
// Signed URL pointing to the extracted client binary in the storage.
ObjectURL client_binary = 3;
// Size of the client binary in bytes.
int64 client_size = 4;
// SHA1 digest of the client binary (as hex string).
//
// Used only by old clients and present here for backward compatibility
// reasons.
//
// Newer clients must verify one of client_ref_aliases instead.
string legacy_sha1 = 5;
// Contains hashes of the client binary calculated using ALL algos supported
// by the server at the time the client package was uploaded.
//
// The callers that want to verify the client binary hash should pick the best
// algo they understand.
//
// The list at least includes 'client_ref' itself and SHA1 hash (matching
// legacy_sha1). The order is undefined.
repeated ObjectRef client_ref_aliases = 6;
}