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=""). // 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; }