l-ai-core-proto/users.proto
Kaien 539983ba17 Add ResolveLinkedAccountByUsername to UsersService
Callers that only know how a person is addressed in chat cannot use the
id-based lookup. Same fail-closed identity rule: an ambiguous username
fails rather than resolving to a guessed user, because acting on the wrong
one can mean phoning someone who never asked to be called.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 16:22:37 +03:00

141 lines
4.6 KiB
Protocol Buffer

syntax = "proto3";
package users;
option go_package = "git.be.kaien.ru/l-ai/l-ai-core-proto/proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
// UsersService manages global users and their linked platform accounts
service UsersService {
rpc CreateGlobalUser(CreateGlobalUserRequest) returns (GlobalUser);
rpc GetGlobalUser(GetGlobalUserRequest) returns (GlobalUser);
rpc GetAllGlobalUsers(google.protobuf.Empty) returns (GetAllGlobalUsersResponse);
rpc UpdateDisplayName(UpdateDisplayNameRequest) returns (google.protobuf.Empty);
rpc LinkAccount(LinkAccountRequest) returns (LinkedAccount);
rpc UnlinkAccount(UnlinkAccountRequest) returns (google.protobuf.Empty);
rpc GetLinkedAccount(GetLinkedAccountRequest) returns (LinkedAccount);
rpc GetLinkedAccountsByPlatform(GetLinkedAccountsByPlatformRequest) returns (GetLinkedAccountsResponse);
rpc GetLinkedAccountsByUserAndPlatform(GetLinkedAccountsByUserAndPlatformRequest) returns (GetLinkedAccountsResponse);
// ResolveLinkedAccount resolves the global user owning a platform account
// (e.g. platform="telegram", account_id="<numeric Telegram user id>").
// This is a read-only, non-creating server-to-server identity lookup used
// by other services (e.g. l-ai-chat Telegram bridge) to map a transport
// identity to the stable global_user_id. It NEVER creates a global user or
// a linked account.
//
// Identity boundary invariant: (platform, account_id) -> at most one
// canonical global_user_id.
// - no rows -> linked=false (account not linked; nothing created)
// - one distinct global user -> that user; if several legacy link rows
// exist for the SAME user, the oldest (smallest linked_account id) wins,
// deterministic across retries
// - more than one distinct global user -> FAILS CLOSED with
// FailedPrecondition (ambiguous identity); callers must not persist
// anything under a guessed identity
rpc ResolveLinkedAccount(ResolveLinkedAccountRequest) returns (ResolveLinkedAccountResponse);
// ResolveLinkedAccountByUsername resolves the global user owning a platform
// account addressed by its username rather than its id (e.g.
// platform="telegram", username="kaien"). Read-only: it never creates a
// global user or a linked account.
//
// Callers that only know how a person is addressed in chat need this; the
// id-based lookup above cannot serve them. Same fail-closed identity rule:
// - no match -> found=false
// - one global user -> that user
// - more than one global user -> FailedPrecondition (ambiguous), so a
// caller never acts on a guessed identity. Acting on the wrong one here
// can mean phoning a person who never asked to be called.
rpc ResolveLinkedAccountByUsername(ResolveLinkedAccountByUsernameRequest) returns (ResolveLinkedAccountResponse);
}
message GlobalUser {
int64 id = 1;
string display_name = 2;
google.protobuf.Timestamp created_at = 3;
google.protobuf.Timestamp updated_at = 4;
repeated LinkedAccount linked_accounts = 5;
}
message LinkedAccount {
int64 id = 1;
int64 global_user_id = 2;
string platform = 3;
string account_id = 4;
string account_username = 5;
string account_url = 6;
google.protobuf.Timestamp linked_at = 7;
google.protobuf.Timestamp updated_at = 8;
}
message CreateGlobalUserRequest {
string display_name = 1;
}
message GetGlobalUserRequest {
int64 id = 1;
}
message GetAllGlobalUsersResponse {
repeated GlobalUser users = 1;
}
message UpdateDisplayNameRequest {
int64 id = 1;
string display_name = 2;
}
message LinkAccountRequest {
int64 global_user_id = 1;
string platform = 2;
string account_id = 3;
string account_username = 4;
string account_url = 5;
}
message UnlinkAccountRequest {
int64 global_user_id = 1;
string platform = 2;
string account_id = 3;
}
message GetLinkedAccountRequest {
int64 global_user_id = 1;
string platform = 2;
}
message GetLinkedAccountsByPlatformRequest {
string platform = 1;
}
message GetLinkedAccountsByUserAndPlatformRequest {
int64 global_user_id = 1;
string platform = 2;
}
message GetLinkedAccountsResponse {
repeated LinkedAccount accounts = 1;
}
message ResolveLinkedAccountRequest {
string platform = 1;
string account_id = 2;
}
message ResolveLinkedAccountByUsernameRequest {
string platform = 1;
// Username without a leading "@"; matched case-insensitively, the way
// people write it in chat.
string username = 2;
}
message ResolveLinkedAccountResponse {
bool linked = 1;
int64 global_user_id = 2;
string display_name = 3;
}