Here is the exact data my client side sends over the wire in the gRPC payload for pub messages:
I/flutter (18878): 10:29:47.420 MAIN > INFO: [TinodeAttachmentService] Starting REST upload to xxxxxxxxxxxxxxxxxxxx/v0/file/u/: file=1000000695.jpg, mime=image/jpeg, size=171122 bytes
I/flutter (18878): 10:29:47.918 MAIN > INFO: [TinodeAttachmentService] POST upload response status: 200
I/flutter (18878): 10:29:47.920 MAIN > INFO: [TinodeService] Publishing attachment to topic usrsM65NyggZjI: file=1000000695.jpg, type=IM, msgId=media_-1781490133266
I/flutter (18878): 10:29:47.923 MAIN > INFO: [TinodeService] Sending ClientMsg [pub]: id=media_-1781490133266, topic=usrsM65NyggZjI, head={mime: "text/x-drafty", attachments: ["/v0/file/s/lbPv-f6sQ8I.jpeg"]}, content={"txt":" ","fmt":[{"at":0,"len":1,"key":0}],"ent":[{"tp":"IM","data":{"mime":"image/jpeg","name":"1000000695.jpg","ref":"/v0/file/s/lbPv-f6sQ8I.jpeg","size":171122}}]}
I/flutter (18878): 10:29:47.925 MAIN > INFO: [TinodeService] extra: attachments=[/v0/file/s/lbPv-f6sQ8I.jpeg], onBehalfOf=, authLevel=NONE
I/flutter (18878): 10:29:47.958 MAIN > FINE: [TinodeService] Received ServerMsg: ServerMsg_Message.ctrl
I/flutter (18878): 10:29:47.959 MAIN > FINE: [TinodeService] Ctrl response: id=, code=403, text=permission denied
I/flutter (18878): 10:30:02.928 MAIN > SEVERE: [TinodeService] Publish attachment to usrsM65NyggZjI timed out or failed.
This is server side
I2026/06/15 02:29:47 media upload: ok lbPv-f6sQ8I swz676p6vrb4e
I2026/06/15 02:29:47 grpc in: pub:{id:"media_-1781490133266" topic:"usrsM65NyggZjI" head:{key:"attachments" value:"[\"/v0/file/s/lbPv-f6sQ8I.jpeg\"]"} head:{key:"mime" value:"\"text/x-drafty\""} content:"{\"txt\":\" \",\"fmt\":[{\"at\":0,\"len\":1,\"key\":0}],\"ent\":[{\"tp\":\"IM\",\"data\":{\"mime\":\"image/jpeg\",\"name\":\"1000000695.jpg\",\"ref\":\"/v0/file/s/lbPv-f6sQ8I.jpeg\",\"size\":171122}}]}"} extra:{attachments:"/v0/file/s/lbPv-f6sQ8I.jpeg"} 8us6qsubZ0Y
W2026/06/15 02:29:47 s.dispatch: non-root assigned asUser 8us6qsubZ0Y
im sure im not sending string
Could the 403 Forbidden error be related to how the server validates the ClientExtra fields in session.go?
Specifically, when the Dart client instantiates ClientExtra, the Protobuf library automatically serializes the default unassigned fields (auth_level: NONE and on_behalf_of: "") into the binary payload. (This is because auth_level is defined as a Protobuf enum where NONE = 0 is the default value).
On the server side, session.go

Since the client sends the default enum value NONE, the server parses msg.Extra.AuthLevel as "NONE". Because "NONE" is not an empty string "", it seems the first condition evaluates to false, which then triggers the root check for standard users.
Is this the expected behavior, and should standard clients explicitly clear/omit these default fields from ClientExtra before sending to avoid the root check?