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>
141 lines
4.6 KiB
Protocol Buffer
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;
|
|
}
|