From b7d5d09f8a7445076811d6f53b98d3445ce3afac Mon Sep 17 00:00:00 2001 From: boojack Date: Mon, 20 Jul 2026 19:41:55 +0800 Subject: [PATCH] fix(api): update UID compatibility Use the original UID format consistently across API, username, and store validation so UUID-based callers continue to work. Regenerate API documentation and add regression coverage for UUID memo IDs. --- internal/base/resource_name.go | 6 +--- internal/base/resource_name_test.go | 29 ++----------------- proto/api/v1/attachment_service.proto | 2 +- proto/api/v1/idp_service.proto | 2 +- proto/api/v1/memo_service.proto | 4 +-- proto/api/v1/user_service.proto | 4 +-- proto/gen/api/v1/attachment_service.pb.go | 2 +- proto/gen/api/v1/idp_service.pb.go | 2 +- proto/gen/api/v1/memo_service.pb.go | 4 +-- proto/gen/api/v1/user_service.pb.go | 4 +-- proto/gen/openapi.yaml | 12 ++++---- server/router/api/v1/resource_name.go | 6 ++-- server/router/api/v1/resource_name_test.go | 9 +++--- server/router/api/v1/sso_username.go | 9 +++--- server/router/api/v1/sso_username_test.go | 5 ++-- .../router/api/v1/test/memo_service_test.go | 21 ++++++++++++++ server/router/api/v1/user_resource_name.go | 16 ++++++++-- .../router/api/v1/user_resource_name_test.go | 14 ++++----- .../proto/api/v1/attachment_service_pb.ts | 2 +- web/src/types/proto/api/v1/idp_service_pb.ts | 2 +- web/src/types/proto/api/v1/memo_service_pb.ts | 4 +-- web/src/types/proto/api/v1/user_service_pb.ts | 4 +-- 22 files changed, 83 insertions(+), 80 deletions(-) diff --git a/internal/base/resource_name.go b/internal/base/resource_name.go index 4de3b595c..14e6fe2d7 100644 --- a/internal/base/resource_name.go +++ b/internal/base/resource_name.go @@ -3,9 +3,5 @@ package base import "regexp" var ( - // ResourceIDMatcher matches new user-provided resource IDs per AIP-122. - ResourceIDMatcher = regexp.MustCompile(`^[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$`) - // UIDMatcher also accepts the previous 36-character format so existing - // generated and persisted UIDs remain writable by the store layer. - UIDMatcher = regexp.MustCompile(`^(?:[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?|[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?)$`) + UIDMatcher = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$`) ) diff --git a/internal/base/resource_name_test.go b/internal/base/resource_name_test.go index 8c17a0e64..9fa926628 100644 --- a/internal/base/resource_name_test.go +++ b/internal/base/resource_name_test.go @@ -1,7 +1,6 @@ package base import ( - "strings" "testing" ) @@ -24,8 +23,8 @@ func TestUIDMatcher(t *testing.T) { {"abc123", true}, {"abc123-", false}, {"123e4567-e89b-12d3-a456-426614174000", true}, // UUID v4 from IDP - {"a123456789012345678901234567890123456", true}, // New AIP-compatible IDs can exceed the legacy limit. - {"A123456789012345678901234567890123456", false}, // Legacy uppercase IDs remain capped at 36 characters. + {"a12345678901234567890123456789012345", true}, // 36 characters. + {"a123456789012345678901234567890123456", false}, // 37 characters. } for _, test := range tests { @@ -37,27 +36,3 @@ func TestUIDMatcher(t *testing.T) { }) } } - -func TestResourceIDMatcher(t *testing.T) { - tests := []struct { - input string - expected bool - }{ - {"a", true}, - {"abc-123", true}, - {"a" + strings.Repeat("b", 62), true}, - {"a" + strings.Repeat("b", 63), false}, - {"1abc", false}, - {"Abc", false}, - {"abc-", false}, - {"abc_def", false}, - } - - for _, test := range tests { - t.Run(test.input, func(t *testing.T) { - if got := ResourceIDMatcher.MatchString(test.input); got != test.expected { - t.Errorf("ResourceIDMatcher.MatchString(%q) = %v, want %v", test.input, got, test.expected) - } - }) - } -} diff --git a/proto/api/v1/attachment_service.proto b/proto/api/v1/attachment_service.proto index 2007bb1b1..6a806bb76 100644 --- a/proto/api/v1/attachment_service.proto +++ b/proto/api/v1/attachment_service.proto @@ -117,7 +117,7 @@ message CreateAttachmentRequest { // Optional. The attachment ID to use for this attachment. // If empty, a unique ID will be generated. - // Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ + // Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$ string attachment_id = 2 [(google.api.field_behavior) = OPTIONAL]; } diff --git a/proto/api/v1/idp_service.proto b/proto/api/v1/idp_service.proto index 34792b968..40ec4e028 100644 --- a/proto/api/v1/idp_service.proto +++ b/proto/api/v1/idp_service.proto @@ -125,7 +125,7 @@ message CreateIdentityProviderRequest { // Optional. The ID to use for the identity provider, which will become the final component of the resource name. // If not provided, the system will generate one. - // Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ + // Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$ string identity_provider_id = 2 [(google.api.field_behavior) = OPTIONAL]; } diff --git a/proto/api/v1/memo_service.proto b/proto/api/v1/memo_service.proto index 3344341cb..e1bfa6656 100644 --- a/proto/api/v1/memo_service.proto +++ b/proto/api/v1/memo_service.proto @@ -205,7 +205,7 @@ message Memo { }; // The resource name of the memo. - // Format: memos/{memo}, memo is the user defined id or uuid. + // Format: memos/{memo}, where memo is the user-defined UID. string name = 1 [(google.api.field_behavior) = IDENTIFIER]; // The state of the memo. @@ -296,7 +296,7 @@ message CreateMemoRequest { // Optional. The memo ID to use for this memo. // If empty, a unique ID will be generated. - // Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ + // Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$ string memo_id = 2 [(google.api.field_behavior) = OPTIONAL]; } diff --git a/proto/api/v1/user_service.proto b/proto/api/v1/user_service.proto index 3cb491d72..e3d897026 100644 --- a/proto/api/v1/user_service.proto +++ b/proto/api/v1/user_service.proto @@ -312,8 +312,8 @@ message CreateUserRequest { ]; // Optional. The resource ID to use for this user. If set, it must equal - // user.username and follow the User resource ID format. - // Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ + // user.username, follow the UID format, and not consist entirely of digits. + // Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$ string user_id = 2 [(google.api.field_behavior) = OPTIONAL]; // Optional. If set, validate the request but don't actually create the user. diff --git a/proto/gen/api/v1/attachment_service.pb.go b/proto/gen/api/v1/attachment_service.pb.go index ca8a398fb..463a7df56 100644 --- a/proto/gen/api/v1/attachment_service.pb.go +++ b/proto/gen/api/v1/attachment_service.pb.go @@ -327,7 +327,7 @@ type CreateAttachmentRequest struct { Attachment *Attachment `protobuf:"bytes,1,opt,name=attachment,proto3" json:"attachment,omitempty"` // Optional. The attachment ID to use for this attachment. // If empty, a unique ID will be generated. - // Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ + // Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$ AttachmentId string `protobuf:"bytes,2,opt,name=attachment_id,json=attachmentId,proto3" json:"attachment_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache diff --git a/proto/gen/api/v1/idp_service.pb.go b/proto/gen/api/v1/idp_service.pb.go index 9f35faca4..70059e72c 100644 --- a/proto/gen/api/v1/idp_service.pb.go +++ b/proto/gen/api/v1/idp_service.pb.go @@ -512,7 +512,7 @@ type CreateIdentityProviderRequest struct { IdentityProvider *IdentityProvider `protobuf:"bytes,1,opt,name=identity_provider,json=identityProvider,proto3" json:"identity_provider,omitempty"` // Optional. The ID to use for the identity provider, which will become the final component of the resource name. // If not provided, the system will generate one. - // Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ + // Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$ IdentityProviderId string `protobuf:"bytes,2,opt,name=identity_provider_id,json=identityProviderId,proto3" json:"identity_provider_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache diff --git a/proto/gen/api/v1/memo_service.pb.go b/proto/gen/api/v1/memo_service.pb.go index 8ba8215ac..713fe8aed 100644 --- a/proto/gen/api/v1/memo_service.pb.go +++ b/proto/gen/api/v1/memo_service.pb.go @@ -219,7 +219,7 @@ func (x *Reaction) GetCreateTime() *timestamppb.Timestamp { type Memo struct { state protoimpl.MessageState `protogen:"open.v1"` // The resource name of the memo. - // Format: memos/{memo}, memo is the user defined id or uuid. + // Format: memos/{memo}, where memo is the user-defined UID. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // The state of the memo. State State `protobuf:"varint,2,opt,name=state,proto3,enum=memos.api.v1.State" json:"state,omitempty"` @@ -472,7 +472,7 @@ type CreateMemoRequest struct { Memo *Memo `protobuf:"bytes,1,opt,name=memo,proto3" json:"memo,omitempty"` // Optional. The memo ID to use for this memo. // If empty, a unique ID will be generated. - // Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ + // Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$ MemoId string `protobuf:"bytes,2,opt,name=memo_id,json=memoId,proto3" json:"memo_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache diff --git a/proto/gen/api/v1/user_service.pb.go b/proto/gen/api/v1/user_service.pb.go index ac46ef3c5..41bcc14ed 100644 --- a/proto/gen/api/v1/user_service.pb.go +++ b/proto/gen/api/v1/user_service.pb.go @@ -651,8 +651,8 @@ type CreateUserRequest struct { // Required. The user to create. User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` // Optional. The resource ID to use for this user. If set, it must equal - // user.username and follow the User resource ID format. - // Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ + // user.username, follow the UID format, and not consist entirely of digits. + // Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$ UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // Optional. If set, validate the request but don't actually create the user. ValidateOnly bool `protobuf:"varint,3,opt,name=validate_only,json=validateOnly,proto3" json:"validate_only,omitempty"` diff --git a/proto/gen/openapi.yaml b/proto/gen/openapi.yaml index 09fd36dba..421ceeedf 100644 --- a/proto/gen/openapi.yaml +++ b/proto/gen/openapi.yaml @@ -95,7 +95,7 @@ paths: description: |- Optional. The attachment ID to use for this attachment. If empty, a unique ID will be generated. - Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ + Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$ schema: type: string requestBody: @@ -350,7 +350,7 @@ paths: description: |- Optional. The ID to use for the identity provider, which will become the final component of the resource name. If not provided, the system will generate one. - Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ + Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$ schema: type: string requestBody: @@ -705,7 +705,7 @@ paths: description: |- Optional. The memo ID to use for this memo. If empty, a unique ID will be generated. - Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ + Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$ schema: type: string requestBody: @@ -1355,8 +1355,8 @@ paths: in: query description: |- Optional. The resource ID to use for this user. If set, it must equal - user.username and follow the User resource ID format. - Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ + user.username, follow the UID format, and not consist entirely of digits. + Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$ schema: type: string - name: validateOnly @@ -3230,7 +3230,7 @@ components: type: string description: |- The resource name of the memo. - Format: memos/{memo}, memo is the user defined id or uuid. + Format: memos/{memo}, where memo is the user-defined UID. state: enum: - STATE_UNSPECIFIED diff --git a/server/router/api/v1/resource_name.go b/server/router/api/v1/resource_name.go index bb1ed3487..bc362dc1e 100644 --- a/server/router/api/v1/resource_name.go +++ b/server/router/api/v1/resource_name.go @@ -136,14 +136,14 @@ func ExtractIdentityProviderUIDFromName(name string) (string, error) { // ValidateAndGenerateUID validates a user-provided UID or generates a new one. // If provided is empty, a new shortuuid is generated. -// If provided is non-empty, it is validated as a user-provided resource ID. +// If provided is non-empty, it is validated against base.UIDMatcher. func ValidateAndGenerateUID(provided string) (string, error) { uid := strings.TrimSpace(provided) if uid == "" { return shortuuid.New(), nil } - if !base.ResourceIDMatcher.MatchString(uid) { - return "", status.Errorf(codes.InvalidArgument, "invalid resource ID: must be 1-63 characters, start with a lowercase letter, contain only lowercase letters, digits, or hyphens, and end with a letter or digit") + if !base.UIDMatcher.MatchString(uid) { + return "", status.Errorf(codes.InvalidArgument, "invalid UID: must be 1-36 characters, contain only letters, digits, or hyphens, and start and end with a letter or digit") } return uid, nil } diff --git a/server/router/api/v1/resource_name_test.go b/server/router/api/v1/resource_name_test.go index 3b25e0f69..ec0dbc810 100644 --- a/server/router/api/v1/resource_name_test.go +++ b/server/router/api/v1/resource_name_test.go @@ -12,10 +12,11 @@ func TestValidateAndGenerateUIDValidatesUserProvidedResourceIDs(t *testing.T) { wantError bool }{ {name: "lowercase", provided: "memo-1"}, - {name: "maximum length", provided: "a" + strings.Repeat("b", 62)}, - {name: "digit first", provided: "1-memo", wantError: true}, - {name: "uppercase", provided: "Memo", wantError: true}, - {name: "too long", provided: "a" + strings.Repeat("b", 63), wantError: true}, + {name: "UUID", provided: "21ec98aa-9a8f-458c-a2a3-c7dc69b6f591"}, + {name: "maximum length", provided: "a" + strings.Repeat("b", 35)}, + {name: "digit first", provided: "1-memo"}, + {name: "uppercase", provided: "Memo"}, + {name: "too long", provided: "a" + strings.Repeat("b", 36), wantError: true}, } for _, test := range tests { diff --git a/server/router/api/v1/sso_username.go b/server/router/api/v1/sso_username.go index 2e7f106d7..0c40b92ec 100644 --- a/server/router/api/v1/sso_username.go +++ b/server/router/api/v1/sso_username.go @@ -8,12 +8,13 @@ import ( // deriveSSOUsername produces the local username for a new SSO-created user. // -// The current policy prefixes a UUID with a letter so the generated value -// follows the same AIP-compatible format as user-selected usernames. +// The current policy is to use a standard UUID string directly. This keeps the +// username independent of IdP profile fields and avoids availability probes or +// retry loops around concurrent first-time logins. func deriveSSOUsername() (string, error) { - username := "user-" + util.GenUUID() + username := util.GenUUID() if err := validateWritableUsername(username); err != nil { - return "", errors.Wrap(err, "generated username did not satisfy username constraints") + return "", errors.Wrap(err, "generated UUID did not satisfy username constraints") } return username, nil } diff --git a/server/router/api/v1/sso_username_test.go b/server/router/api/v1/sso_username_test.go index 02a29c3a5..c576a28f9 100644 --- a/server/router/api/v1/sso_username_test.go +++ b/server/router/api/v1/sso_username_test.go @@ -1,7 +1,6 @@ package v1 import ( - "strings" "testing" ) @@ -10,8 +9,8 @@ func TestDeriveSSOUsername(t *testing.T) { if err != nil { t.Fatalf("deriveSSOUsername() returned error: %v", err) } - if !strings.HasPrefix(username, "user-") { - t.Fatalf("deriveSSOUsername() = %q, want user- prefix", username) + if len(username) != 36 { + t.Fatalf("deriveSSOUsername() = %q, want a 36-character UUID", username) } if err := validateWritableUsername(username); err != nil { t.Fatalf("deriveSSOUsername() produced invalid username %q: %v", username, err) diff --git a/server/router/api/v1/test/memo_service_test.go b/server/router/api/v1/test/memo_service_test.go index d9f3fe5d5..aab6ce333 100644 --- a/server/router/api/v1/test/memo_service_test.go +++ b/server/router/api/v1/test/memo_service_test.go @@ -17,6 +17,27 @@ import ( "github.com/usememos/memos/store" ) +func TestCreateMemoAcceptsUUID(t *testing.T) { + ctx := context.Background() + ts := NewTestService(t) + defer ts.Cleanup() + + user, err := ts.CreateRegularUser(ctx, "test-user") + require.NoError(t, err) + userCtx := ts.CreateUserContext(ctx, user.ID) + + const memoID = "21ec98aa-9a8f-458c-a2a3-c7dc69b6f591" + memo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{ + Memo: &apiv1.Memo{ + Content: "Created with a UUID", + Visibility: apiv1.Visibility_PRIVATE, + }, + MemoId: memoID, + }) + require.NoError(t, err) + require.Equal(t, "memos/"+memoID, memo.Name) +} + func TestListMemos(t *testing.T) { ctx := context.Background() diff --git a/server/router/api/v1/user_resource_name.go b/server/router/api/v1/user_resource_name.go index 8ca77e443..5ac408ed4 100644 --- a/server/router/api/v1/user_resource_name.go +++ b/server/router/api/v1/user_resource_name.go @@ -27,12 +27,24 @@ func parseUsernameFromName(name string) (string, error) { } func validateWritableUsername(username string) error { - if !base.ResourceIDMatcher.MatchString(username) { - return errors.New("invalid username: must be 1-63 characters, start with a lowercase letter, contain only lowercase letters, digits, or hyphens, and end with a letter or digit") + if username == "" || isNumericUsername(username) || !base.UIDMatcher.MatchString(username) { + return errors.Errorf("invalid username %q", username) } return nil } +func isNumericUsername(username string) bool { + if username == "" { + return false + } + for _, char := range username { + if char < '0' || char > '9' { + return false + } + } + return true +} + // ResolveUserByName resolves a username-based user resource name to a store user. func ResolveUserByName(ctx context.Context, stores *store.Store, name string) (*store.User, error) { username, err := parseUsernameFromName(name) diff --git a/server/router/api/v1/user_resource_name_test.go b/server/router/api/v1/user_resource_name_test.go index cea4c3e7e..5becff34b 100644 --- a/server/router/api/v1/user_resource_name_test.go +++ b/server/router/api/v1/user_resource_name_test.go @@ -16,9 +16,8 @@ func TestValidateWritableUsername(t *testing.T) { username: "alice", }, { - name: "mixed case", - username: "Alice", - wantError: true, + name: "mixed case", + username: "Alice", }, { name: "hyphenated", @@ -30,17 +29,16 @@ func TestValidateWritableUsername(t *testing.T) { }, { name: "maximum length", - username: "a" + strings.Repeat("b", 62), + username: "a" + strings.Repeat("b", 35), }, { name: "too long", - username: "a" + strings.Repeat("b", 63), + username: "a" + strings.Repeat("b", 36), wantError: true, }, { - name: "digit first", - username: "1alice", - wantError: true, + name: "digit first", + username: "1alice", }, { name: "hyphen last", diff --git a/web/src/types/proto/api/v1/attachment_service_pb.ts b/web/src/types/proto/api/v1/attachment_service_pb.ts index c6fb9dcc0..9e5fcc6c3 100644 --- a/web/src/types/proto/api/v1/attachment_service_pb.ts +++ b/web/src/types/proto/api/v1/attachment_service_pb.ts @@ -146,7 +146,7 @@ export type CreateAttachmentRequest = Message<"memos.api.v1.CreateAttachmentRequ /** * Optional. The attachment ID to use for this attachment. * If empty, a unique ID will be generated. - * Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ + * Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$ * * @generated from field: string attachment_id = 2; */ diff --git a/web/src/types/proto/api/v1/idp_service_pb.ts b/web/src/types/proto/api/v1/idp_service_pb.ts index 8cc37dff7..7ee474f03 100644 --- a/web/src/types/proto/api/v1/idp_service_pb.ts +++ b/web/src/types/proto/api/v1/idp_service_pb.ts @@ -257,7 +257,7 @@ export type CreateIdentityProviderRequest = Message<"memos.api.v1.CreateIdentity /** * Optional. The ID to use for the identity provider, which will become the final component of the resource name. * If not provided, the system will generate one. - * Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ + * Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$ * * @generated from field: string identity_provider_id = 2; */ diff --git a/web/src/types/proto/api/v1/memo_service_pb.ts b/web/src/types/proto/api/v1/memo_service_pb.ts index 75e3b2a23..560aa1209 100644 --- a/web/src/types/proto/api/v1/memo_service_pb.ts +++ b/web/src/types/proto/api/v1/memo_service_pb.ts @@ -79,7 +79,7 @@ export const ReactionSchema: GenMessage = /*@__PURE__*/ export type Memo = Message<"memos.api.v1.Memo"> & { /** * The resource name of the memo. - * Format: memos/{memo}, memo is the user defined id or uuid. + * Format: memos/{memo}, where memo is the user-defined UID. * * @generated from field: string name = 1; */ @@ -292,7 +292,7 @@ export type CreateMemoRequest = Message<"memos.api.v1.CreateMemoRequest"> & { /** * Optional. The memo ID to use for this memo. * If empty, a unique ID will be generated. - * Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ + * Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$ * * @generated from field: string memo_id = 2; */ diff --git a/web/src/types/proto/api/v1/user_service_pb.ts b/web/src/types/proto/api/v1/user_service_pb.ts index 340e56ec9..def729c15 100644 --- a/web/src/types/proto/api/v1/user_service_pb.ts +++ b/web/src/types/proto/api/v1/user_service_pb.ts @@ -293,8 +293,8 @@ export type CreateUserRequest = Message<"memos.api.v1.CreateUserRequest"> & { /** * Optional. The resource ID to use for this user. If set, it must equal - * user.username and follow the User resource ID format. - * Format: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ + * user.username, follow the UID format, and not consist entirely of digits. + * Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$ * * @generated from field: string user_id = 2; */