主题
XHIM Server
第一次部署请从 Docker 快速部署 开始;已有服务需要绑定 公网地址时直接阅读 自定义域名与 HTTPS。不要从高级发布 审计手册倒推安装步骤。
先选择要做的事情
| 目标 | 阅读页面 |
|---|---|
| 在一台服务器上启动联调环境 | Docker 快速部署 |
| 配置宝塔、Nginx、Caddy、域名和 HTTPS | 自定义域名与 HTTPS |
| 不使用 Docker | 非 Docker 部署 |
| 部署多副本生产集群 | Kubernetes / Helm 高可用部署 |
| 升级、迁移、回滚或发布到现网 | 生产发布、升级与回滚 |
| 让业务后台创建用户、发通知或导入历史数据 | 服务端到服务端 API |
客户端开发者不需要执行本页的部署命令;服务端负责人完成验收后,只需要向客户 端团队交付一个 XHIM Server URL 和对应环境的登录方式。
XHIM Server 是 XHIM 客户端 SDK 的参考服务端。它与仓库内的 C++ Product Adapter 使用同一套 Protobuf v1 契约,当前已经跑通:
text
Token → Session Bootstrap → HTTPS/WSS → initial Sync → Ready
→ local Outbox → send commit → realtime hint → Sync → SQLite merge本页面向服务端部署、运维和 XHIM 产品交付人员。后续内容用于查阅服务能力和 配置项;第一次安装、配置域名和上线发布应分别按上表中的任务页操作,不应要求 iOS、Android、Windows 或 HarmonyOS 开发者执行这些命令。
XHIM 销售方不需要在销售前替购买方配置真实生产环境。购买方取得服务端源码和 部署文件后,可以自主设置域名或 IP、数据库、对象存储、Push 和管理员认证。 XHIM 交付包只提供安全的配置入口和默认模板,不内置任何客户密钥。 安装、更新和回滚步骤均在本页按任务列出。产品版本和购买说明见 XHIM 产品页。
客户端开发者应直接阅读对应平台 QuickStart,例如 iOS 从零接入。
交付给客户端团队的联调信息
服务端部署并初始化完成后,服务端负责人只向客户端团队交付可公开使用的接入 信息,不交付 Docker 命令、数据库连接、Admin Key 或签名私钥。
Development 联调交付模板:
text
XHIM Server URL: https://im-test.customer.com
Test User IDs: alice, bob
Test Conversation ID: xhim-demo-direct
Login Mode: Development Easy Login
Phone Demo Authentication: enabled and verifiedProduction 交付模板:
text
XHIM Server URL: https://im.customer.com
User ID Source: 当前业务登录账号的稳定用户 ID
Business Authentication Callback: 已接入
Login Mode: Business服务端负责人交付前应自行确认:
- Server URL 可以从目标手机或桌面设备访问;
- Development 测试账号和测试会话已经创建且成员关系正确;
- 需要手机号 Demo 时,公开配置明确返回
development_phone_auth_enabled=true,并已实际完成注册和密码登录; - Production 域名、证书、API、WebSocket 和媒体地址均已就绪;
- 业务鉴权接口会根据当前业务登录态签发对应用户的 XHIM 登录票据;
- 管理凭证、数据库地址和签名密钥没有进入客户端交付物。
客户端团队拿到上述信息后,只配置平台 SDK,不参与服务端部署。两端问题的边界 是:客户端负责 SDK 初始化、系统权限和 UI;服务端负责网络可达、账号、会话、 鉴权、存储和运行状态。
正式业务鉴权的实现细节只需要购买方服务端人员理解。客户端人员看到的是一个 回调,SDK 负责登录票据的提交、过期续期和并发合并。
当前服务能力
- Ed25519 JWT 用户 Token 和 Scope 校验;
- 带过期时间、签名和明确 Key ID 的 Endpoint Bundle;
- 用户、单聊/群聊会话的管理端初始化 API;
- 嵌入式自托管管理后台:运行概览、部署公开配置、用户、会话、登录设备、 测试 Token 和安全审计;
- 好友申请、接受/拒绝、好友快照,以及账号流增量关系事件;
- 客户端建群、Owner 权限、动态成员增删、群 revision 乐观并发控制和成员分页;
- 黑名单、入群审批、管理员、成员禁言、群主转让与治理审计;
(sender_user_id, client_message_id)服务端幂等;- 会话内单调
server_seq; - 会话历史按
server_seq倒序 keyset 分页,读取快照同时返回最新序号和当前 用户视图; - 消息“仅为我删除”、会话清空高水位和会话隐藏高水位,支持 mutation 幂等、revision CAS 和同账号多设备同步;隐藏会话收到更高序号的新消息后自动 恢复显示;
- 账号私有的会话偏好全量替换:置顶、免打扰和最大 16 KiB 的 规范 JSON object 业务扩展共用 exact revision CAS、mutation ID 幂等和 账号私有 Sync;空扩展表示显式清除,审计不记录扩展内容;
- 按用户和会话持久化、带 revision CAS 的单调已读序号;当前账号设备同步
ConversationReadUpsert,其他会话成员同步ConversationPeerReadUpsert;支持一次事务把当前用户全部有权会话推进到调用 快照的最新序号,并精确幂等回放; - 服务端设备会话注册表、
multi/single/single_per_platform登录策略、确定性容量淘汰、用户主动撤销和旧连接实时失效; - 基于设备租约聚合的 Presence 与 Typing 临时事件,支持断线/撤销清理、 TTL 过期、跨实例投递和黑名单/成员权限边界;
- 每账号单调事件流、HMAC 不透明 Cursor 和分页 Sync;
- 二进制 Protobuf HTTP API;
- WSS 实时 Sync Hint、会话撤销、Presence、Typing、Ping/Pong 和断线恢复入口;
- PostgreSQL
LISTEN/NOTIFY跨副本 Hint 总线、租户隔离和周期兜底 Hint; - 会话权限绑定的媒体控制面:Prepare、短期上传授权、服务端完整性提交和 短期下载授权;
- 开发环境 HMAC 签名本地对象存储和生产 S3/S3-compatible 预签名 Adapter;
- 每客户端 IP 令牌桶、WebSocket 并发上限、请求 ID、结构化访问日志和基础 Prometheus 指标;
- PostgreSQL 持久化实现、内存测试实现和嵌入式迁移;
- 设备 Push 注册、耐久 Outbox、租约/退避 Worker、可替换 Webhook Provider, 以及 APNs/FCM/Huawei Push Kit 直连 Provider;
- 持久化通话邀请/接受/拒绝/挂断信令、账号增量信令分页和每用户短期签名 RTC Token;
- Caddy TLS 入口、Docker Compose、本机直连 TLS 和优雅关闭;
- Go race test、HTTP/WebSocket 纵向测试和 C++/Go 跨语言 E2E。
用户与群聊二维码
登录客户端可使用以下公开业务端点:
text
POST /v1/qrcodes:create
POST /v1/qrcodes:resolve创建接口签发带 App 隔离和有效期的 AES-256-GCM 不透明载荷;解析接口在服务端 重新校验目标、成员关系和权限。二维码中不直接包含内部用户 ID、群 ID、用户 Token 或管理密钥。完整请求、响应、客户端入口和密钥轮换规则见 docs/QR_CODES.md。
当前管理后台面向客户私有化部署,不包含厂商侧 SaaS 计费、订单、跨客户租户或 多区域运维控制面。黑名单、入群申请、管理员/禁言/转让、耐久 Push Outbox、 Webhook/APNs/FCM/Huawei Push Provider、持久化通话信令和用户级 RTC Token 已实现;媒体转码、 外部内容审核厂商和具体 RTC 厂商/SFU 集群通过配置的 Provider 接入,应用侧只需 使用 SDK 的媒体与通话方法。
一条命令验证 SDK 与服务端
在 macOS 开发机执行:
bash
./scripts/run_server_e2e.sh脚本会:
- 创建一次性 Ed25519/HMAC 密钥和带 SAN 的本机 TLS 证书;
- 启动内存模式 XHIM Server;
- 创建 Alice、Bob 和一个单聊会话并签发两人的 Token;
- 构建嵌入 reference Product Adapter 的正式模式 C ABI;
- 验证登录、WSS、初始同步、发送、ACK/Sync 回流、本地
SERVER_ACCEPTED,以及服务端权威 markRead 回包和变更事件; - 由 Alice 准备并上传媒体,服务端校验 SHA-256 后提交,再由 Bob 通过会话 成员鉴权取得下载授权并校验原始字节;
- 停止服务并删除临时证书、Token、媒体文件和数据库。
脚本不会关闭 TLS 校验,也不会使用 LOCAL_PREVIEW。
本机运行 Go 测试
bash
cd server
go test -race ./...
go vet ./...协议变更后,在仓库根目录执行:
bash
buf lint
buf generate
git diff --exit-code -- server/gen不使用 Docker 部署
客户运维团队可以直接使用 Go 二进制、PostgreSQL、systemd 和宝塔/ Nginx,不需要安装 Docker。完整目录、最小权限、配置、反向代理、 升级和回滚步骤见 XHIM Server 非 Docker 部署。
Docker Compose 开发环境
国内网络构建镜像
server/Dockerfile 默认使用 Go 官方模块代理。部署服务器无法访问该代理时, 通过构建参数切换到部署方批准且可访问的模块代理,不要修改源码或关闭 GOSUMDB:
bash
docker build \
--build-arg XHIM_GOPROXY=https://goproxy.cn,direct \
--build-arg XHIM_VERSION=0.1.0-dev.10 \
--build-arg XHIM_COMMIT="$(git rev-parse HEAD)" \
-t xhimsdk-server:0.1.0-dev.10 \
server正式制品必须记录源码提交和实际代理地址;代理只参与依赖下载,依赖内容仍由 仓库内 go.sum 和 Go checksum database 校验。 只有在目标主机已缓存并校验基础镜像时,自托管 Development 发布才可使用 XHIM_DOCKER_BUILD_PULL=0;它不是绕过供应链校验的开关。宝塔 bind 部署脚本 scripts/deploy/deploy_xhim_server_bind.sh 会从当前容器重建已批准的 Healthcheck 命令与时序,并在切换前保留可回滚容器和媒体备份。
Demo 手机号注册登录
以下两项配置同时成立时,Server 会开放 Demo 账号接口;再配置 SMS Webhook 时开启真实手机验证:
dotenv
XHIM_SERVER_ENV=development
XHIM_ENABLE_DEVELOPMENT_LOGIN=true
XHIM_SMS_MODE=webhook
XHIM_SMS_WEBHOOK_URL=https://sms-adapter.example.com/v1/xhim/send
XHIM_SMS_WEBHOOK_KEY_ID=xhim-sms-1
XHIM_SMS_WEBHOOK_SECRET_BASE64=<32 字节以上随机密钥的 Base64>接口为:
text
POST /v1/sdk/development:register
POST /v1/sdk/development:password-login
POST /v1/sdk/development/phone-verifications:request
POST /v1/sdk/development/account:change-phone
POST /v1/sdk/development/account:change-password
POST /v1/sdk/development/account:reset-password注册请求使用 phone_number、password、可选 display_name、device_id 和 platform;成功响应包含 user_id 与 access_token,iOS Demo 会自动完成 后续 IM 连接。Server 不保存明文手机号或密码:手机号使用由服务端持久密钥派生 的 HMAC 摘要索引,密码使用随机盐 PBKDF2-SHA256 校验值。请持久配置 XHIM_CURSOR_KEY_BASE64,否则开发服务重启后旧手机号摘要将无法匹配。
开启 XHIM_SMS_MODE=webhook 后,注册、找回密码、修改手机号和修改密码都必须先 请求一次性验证码。请求只返回 verification_id、有效时间和可重发时间, 不会把验证码返回 App。验证码5分钟有效、60秒内不可重发、最多输入5次,同一 手机号和用途每24小时默认最多10次。服务端只持久化手机号和验证码的密钥摘要, 验证码成功使用后立即失效。
SMS Webhook 是服务端内部适配层,不是第三方公开回调。XHIM 以 HMAC-SHA256 签名请求,内部适配器再调用阿里云、腾讯云、华为云或客户选定的短信供应商。 短信签名、模板和供应商 AccessKey 只能保存在该内部适配器的 Secret 管理中, 不得下发到 Demo、Web 或 SDK。当 SMS 供应商不可用时,接口返回稳定 503 sms_unavailable,不会回退到固定验证码。
内部适配器接收 POST JSON:
json
{
"schema_version": 1,
"verification_id": "phone_verification_...",
"app_id": "app-1",
"purpose": "register",
"phone_number": "+8613800138000",
"code": "123456",
"expires_in_seconds": 300
}请求头包含 X-XHIM-SMS-Version: v1、X-XHIM-SMS-Key-ID、Unix 秒时间戳 X-XHIM-SMS-Timestamp、Idempotency-Key: <verification_id>,以及 X-XHIM-SMS-Signature: v1=<hex HMAC-SHA256>。签名原文为 <timestamp>.<原始 JSON bytes>。适配器必须校验签名、拒绝过期时间戳并按 Idempotency-Key 精确幂等;只有供应商明确受理后才能返回 2xx。
这些接口有独立限流,并在 Production 配置下完全不存在;它们用于官方 Demo 和验收。正式客户产品仍应由自己的业务账号服务签发 XHIM Credential,并按 业务风控、账号申诉和合规要求管理账号。
两个账号修改接口要求当前 Bearer Token 和 current_password。更换手机号还需 new_phone_number,修改密码还需 new_password;成功不会改变 user_id。 它们和注册接口一样只属于 Development Demo。服务端只替换 HMAC 手机摘要或 PBKDF2 校验值,不存储明文,也不提供“读取明文手机号”接口。
手机号 Demo 必须“服务端先发布、客户端后安装”。服务端升级和数据库迁移完成 后,先执行发布审计门禁:
bash
XHIM_PUBLIC_URL=https://im-test.customer.com
curl -fsS "$XHIM_PUBLIC_URL/health/ready"
curl -fsS "$XHIM_PUBLIC_URL/v1/sdk/config" |
jq -e '
.schema_version == 1 and
.environment == "development" and
.development_phone_auth_enabled == true and
.development_sms_verification_enabled == true
'然后使用测试手机号实际完成一次 POST /v1/sdk/development:register 和 POST /v1/sdk/development:password-login,确认两个请求均成功后才能交付新版 Demo。旧服务缺少能力字段时,客户端按“不支持”处理;禁止先发客户端,再让 客户端绕过探测或回退到固定 Token。
登录后的客户端可通过以下 protobuf 接口按手机号精确查找一个用户:
text
POST /v1/users/phone:resolve
Content-Type: application/x-protobuf
Authorization: Bearer <access_token>请求使用 ResolveUserByPhoneRequest,手机号沿用注册登录的规范化规则:中国大陆 11 位手机号可直接输入,也可传完整 E.164。服务端只在当前 Bearer Token 对应的 app_id 内匹配一个 HMAC 摘要,不执行前缀、模糊或批量搜索。成功响应 ResolveUserByPhoneResponse.profile 只包含添加好友所需的公开资料,不回传 手机号、摘要或内部 account_id;未找到返回稳定 not_found。
该接口要求有效设备会话及 social:read scope,并叠加独立的 IP 与账号双重 限流;所有响应均为 Cache-Control: no-store,访问日志不会记录请求体或 手机号。/v1/sdk/config 只在对应服务可用时发布 user.phone_resolution 能力。
同一局域网快速联调(不需要 Tailscale)
确认 Mac 与 iPhone 在同一 Wi-Fi,取得 Mac 的局域网 IPv4:
bash
ipconfig getifaddr en0然后用该地址生成仅限 Development 的明文 HTTP/WS 配置。下面的 IP 只是示例:
bash
scripts/dev/configure_server_env.sh \
--host 192.168.2.250 \
--port 18080 \
--transport lan-http \
--force
docker compose --env-file server/.env \
-f server/compose.yaml up -d --build
curl http://192.168.2.250:18080/health/ready
scripts/dev/provision_chat_demo.sh此模式会把 18080 绑定到 Mac 的所有网卡,并显式生成 XHIM_ALLOW_INSECURE_DEVELOPMENT_TRANSPORT=true。服务端仅在 XHIM_SERVER_ENV=development 时接受该开关;Apple Development 制品也必须在 构建期读取匹配的 server/.env.client 才会允许 http:///ws://。Production 构建会直接拒绝这一开关。
这条路径不依赖 Tailscale,也不需要在手机安装本地 CA,但只允许用于受信任的 隔离测试网络。不要在公共 Wi-Fi、互联网端口映射或正式环境中使用;联调结束后 应停止 Compose 或切回 HTTPS。若真机无法连接,依次检查:
- Mac 与手机是否处于同一网段且路由器未启用客户端隔离;
- macOS 防火墙是否允许 Docker/XHIM 接受入站连接;
curl http://<Mac局域网IP>:18080/health/ready在其他同网设备是否可达;- iOS 首次弹出的“本地网络”权限是否已允许。
本机可信 HTTPS
先生成开发密钥。仅在 Mac 本机联调时使用 localhost;真机 HTTPS 联调必须 传入手机可访问且证书受系统信任的开发域名:
bash
scripts/dev/configure_server_env.sh --host localhost --port 8443脚本会生成被 Git 忽略且权限为 0600 的 server/.env,以及只包含客户端公开 构建参数的 server/.env.client。轮换本地环境时显式使用 --force,该操作会 使旧 Token 和旧客户端 Endpoint 签名公钥失效。随后运行:
bash
cd server
docker compose up --build
docker compose cp \
gateway:/data/caddy/pki/authorities/local/root.crt \
./xhim-local-ca.crt
curl --cacert ./xhim-local-ca.crt \
https://localhost:8443/health/readyCaddy 的 tls internal 只适合本机开发。最简单的浏览器验证可以临时使用 curl -k,但 SDK 联调和生产环境不得关闭证书校验;应导出并显式信任开发 CA, 或改用组织签发的证书。
可选:Tailscale 私网 HTTPS
Tailscale 不是 XHIM 的运行依赖。只有团队希望跨网络保留私网可信 HTTPS 时才 需要它:
bash
scripts/dev/configure_server_env.sh \
--host <mac-name>.<tailnet>.ts.net \
--port 443 \
--force
docker compose up -d --build
# Compose 只把后端映射到 Mac 的 127.0.0.1:18080。
tailscale serve --bg --https=443 http://127.0.0.1:18080
curl https://<mac-name>.<tailnet>.ts.net/health/ready首次启用 Tailscale Serve/HTTPS 需要 Tailnet 管理员在控制台确认。它、局域网 HTTP 和 Caddy :8443 是三种可选开发入口;不要启用 Funnel,除非明确需要把 测试服务暴露到公网。Endpoint 或签名密钥变更后,重启 Server 并把新的 Key ID/ 公钥更新到 App 的 XHIMDeployment;无需因为 API/WSS 域名变化重新构建 SDK。 签名私钥轮换后应重新签发 Demo Token:
bash
scripts/dev/provision_chat_demo.sh该脚本幂等复用 Alice、Bob 与 xhim-demo-direct,Token 只写入被 Git 忽略且 权限为 0600 的 server/.env.demo,不会打印到终端。
生产部署不应照搬开发密码。至少需要:
XHIM_SERVER_ENV=production;- Server、PostgreSQL、Gateway 三个镜像均使用 Registry
image@sha256,不使用可变 Tag; POSTGRES_PASSWORD由 Secret Manager 注入并与XHIM_DATABASE_URL中密码一致;禁止开发默认值;- PostgreSQL TLS、备份、PITR 和连接池容量规划;
- S3 Bucket 私有策略、版本/生命周期、跨区复制、KMS 加密和凭证轮换;
- 独立生成并托管的 Ed25519 私钥、Cursor HMAC Key、32 字节以上 Admin API Key 和管理后台 Session Key;
- 为管理后台设置独立管理员账号及 bcrypt cost 12 以上的密码哈希;
- 由网关、Ingress 或服务本身终止 TLS;
- 对公网 API 与管理 API 分网、认证、限流和审计;
- 多副本实时 fan-out、Push、指标、告警和灾备。
配置
| 变量 | 用途 |
|---|---|
XHIM_SERVER_ENV | development、test 或 production |
XHIM_LISTEN_ADDRESS | HTTP/TLS 监听地址 |
XHIM_GATEWAY_BIND_ADDRESS | Caddy 开发 TLS 入口绑定地址,默认仅回环 |
XHIM_SERVER_LOOPBACK_PORT | Compose 暴露给本机可信反向代理的回环端口 |
XHIM_SERVER_BIND_ADDRESS | Compose 端口绑定地址;默认 127.0.0.1,局域网开发为 0.0.0.0 |
XHIM_ALLOW_INSECURE_DEVELOPMENT_TRANSPORT | 仅 development 可显式允许 HTTP/WS;生产禁止 |
XHIM_E2EE_ENABLED | 服务端权威的端到端加密开关,默认 true;客户端只显示状态,不能开启、关闭或降级 |
XHIM_STORAGE_DRIVER | memory 或 postgres;生产只允许 PostgreSQL |
XHIM_DATABASE_URL | PostgreSQL DSN |
POSTGRES_PASSWORD | Compose PostgreSQL 角色密码;生产由 Secret Manager 注入,并与 DSN 一致 |
XHIM_POSTGRES_IMAGE | PostgreSQL 镜像;生产必须是 image@sha256 |
XHIM_GATEWAY_IMAGE | Caddy Gateway 镜像;生产必须是 image@sha256 |
XHIM_PRODUCT_EDITION | standard 或 enterprise 运行时边界 |
XHIM_DEPLOYMENT_ID | 绑定离线许可证的稳定客户部署 ID,不随容器/Pod 重建变化 |
XHIM_LICENSE_ENFORCEMENT | disabled、warn 或正式交付使用的 required |
XHIM_LICENSE_FILE | Ed25519 签名离线许可证 JSON 路径 |
XHIM_LICENSE_REVOCATION_FILE | 可选的 Ed25519 签名撤销清单;私有部署更新后重启生效 |
XHIM_LICENSE_VERIFY_KEYS_BASE64 | 许可证发行公钥环,格式为 key-id=base64-public-key |
XHIM_ADMIN_KEY | 仅供受信运维自动化调用 Admin API;生产至少 32 字节,不用于网页登录或客户业务集成 |
XHIM_BUSINESS_API_KEY | 可选的客户业务后台凭证;配置后启用 /v1/server/*,至少 32 字节且必须与 Admin Key 不同,严禁放入 App/Web |
XHIM_ADMIN_USERNAME | 管理后台登录账号 |
XHIM_ADMIN_PASSWORD_BCRYPT_BASE64 | 管理员密码 bcrypt 哈希的 Base64,cost 必须不小于 12 |
XHIM_ADMIN_SESSION_KEY_BASE64 | 签名管理后台会话的独立随机密钥,解码后至少 32 字节 |
XHIM_ADMIN_SESSION_LIFETIME | 管理后台会话有效期,默认 8h,可配置 15m—24h |
XHIM_SIGNING_PRIVATE_KEY_BASE64 | Ed25519 seed/private key |
XHIM_CURSOR_KEY_BASE64 | HMAC Cursor Key;生产至少 32 字节 |
XHIM_PUBLIC_API_URL | Endpoint Bundle 中的 HTTPS API 根地址 |
XHIM_PUBLIC_WEBSOCKET_URL | Endpoint Bundle 中的 WSS 地址 |
XHIM_PUBLIC_UPLOAD_URL | 媒体上传服务根地址 |
XHIM_PUBLIC_MEDIA_URL | 媒体下载服务根地址 |
XHIM_ALLOWED_ORIGINS | Web SDK 允许的浏览器来源,逗号分隔的精确 host[:port] 或审核过的通配主机;生产禁止 * |
XHIM_TLS_CERTIFICATE_FILE | 可选,服务直接终止 TLS 时的证书 |
XHIM_TLS_PRIVATE_KEY_FILE | 与证书成对配置的私钥 |
XHIM_REQUEST_RATE | 每实例、每客户端 IP 的持续请求速率 |
XHIM_REQUEST_BURST | 每客户端 IP 的突发请求容量 |
XHIM_MAX_WEBSOCKETS | 每实例允许的活跃 WebSocket 上限 |
XHIM_MULTI_LOGIN_POLICY | multi、single 或 single_per_platform,默认 multi |
XHIM_MAX_SESSIONS_PER_USER | 每个 App + 用户最多保留的活跃设备会话,默认 10,最大 100 |
XHIM_TRUST_PROXY_HEADERS | 仅在服务只接受可信代理流量时启用 |
XHIM_MEDIA_STORAGE_DRIVER | local 或 s3;生产只允许 s3 |
XHIM_MEDIA_LOCAL_ROOT | 本地开发对象目录;Compose 使用持久 Volume |
XHIM_MEDIA_URL_KEY_BASE64 | 本地开发短期 URL 的独立 HMAC Key |
XHIM_MEDIA_AUTH_LIFETIME | 上传/下载授权有效期,最大 1 小时 |
XHIM_S3_REGION | S3 Region |
XHIM_S3_BUCKET | 私有媒体 Bucket |
XHIM_S3_BASE_ENDPOINT | 可选,MinIO 等 S3-compatible Endpoint |
XHIM_S3_USE_PATH_STYLE | 自定义服务需要 Path-style 时启用 |
XHIM_S3_EXPECTED_OWNER | 可选,AWS S3 Bucket Owner 防误配约束 |
XHIM_PUSH_MODE | disabled、webhook 或 direct;未设置时兼容旧 Webhook 配置 |
XHIM_PUSH_WEBHOOK_URL | Webhook 模式的聚合服务;Direct 模式可选作 Web Push Provider |
XHIM_PUSH_WEBHOOK_TOKEN | Push Webhook Bearer Token;生产使用 Webhook 时至少 32 字节 |
XHIM_PUSH_APNS_TEAM_ID | Apple Developer Team ID |
XHIM_PUSH_APNS_KEY_ID | APNs Token Auth Key ID |
XHIM_PUSH_APNS_TOPIC | 可选固定 Bundle ID;未设置时使用设备登记的 App ID |
XHIM_PUSH_APNS_PRIVATE_KEY_BASE64 | APNs .p8 PEM 文件的 Base64 内容 |
XHIM_PUSH_FCM_SERVICE_ACCOUNT_JSON_BASE64 | FCM 服务账号 JSON 的 Base64 内容 |
XHIM_PUSH_HUAWEI_APP_ID | Huawei Push Kit App ID |
XHIM_PUSH_HUAWEI_CLIENT_ID | Huawei OAuth Client ID |
XHIM_PUSH_HUAWEI_CLIENT_SECRET | Huawei OAuth Client Secret |
XHIM_PUSH_REQUEST_TIMEOUT | 单次 Provider 请求超时 |
XHIM_PUSH_BATCH_SIZE | 每次租约批量 |
XHIM_PUSH_MAX_ATTEMPTS | 单条通知最大 Provider 尝试次数,默认 12 |
XHIM_PUSH_LEASE_DURATION | Push 任务执行租约 |
XHIM_PUSH_IDLE_INTERVAL | 空队列轮询间隔 |
XHIM_PUSH_MAX_DEVICES_PER_USER | 每个 App + 账号 + 用户的设备注册硬上限,默认 20,最大 100 |
XHIM_PUSH_DELIVERY_CONCURRENCY | Provider 固定 worker pool 并发上限,默认 8,最大 64 |
XHIM_WEBHOOK_URL | 可选的耐久业务事件 Webhook HTTPS 地址;不允许 URL 凭证、Query 或 Fragment |
XHIM_WEBHOOK_KEY_ID | Webhook HMAC Key ID,用于接收方密钥轮换 |
XHIM_WEBHOOK_SECRET_BASE64 | Webhook HMAC-SHA256 Key 的 Base64,启用时至少 32 字节 |
XHIM_WEBHOOK_REQUEST_TIMEOUT | 单次业务 Webhook HTTP 超时,必须小于租约 |
XHIM_WEBHOOK_BATCH_SIZE | 每轮最多租约的业务事件数,默认 100,最大 500 |
XHIM_WEBHOOK_MAX_ATTEMPTS | 自动投递最大尝试次数,默认 12 |
XHIM_WEBHOOK_LEASE_DURATION | 投递尝试租约;进程重启后过期任务可回收 |
XHIM_WEBHOOK_IDLE_INTERVAL | 空队列轮询间隔 |
XHIM_WEBHOOK_BASE_BACKOFF | Full Jitter 指数退避初始上限 |
XHIM_WEBHOOK_MAX_BACKOFF | Full Jitter 指数退避最大上限 |
XHIM_WEBHOOK_DELIVERY_CONCURRENCY | 固定投递 worker 并发上限,默认 8,最大 64 |
XHIM_POLICY_WEBHOOK_URL | Enterprise/OEM 提交前业务策略 HTTPS 地址 |
XHIM_POLICY_WEBHOOK_KEY_ID | 前置策略 HMAC Key ID |
XHIM_POLICY_WEBHOOK_SECRET_BASE64 | 前置策略 HMAC Key 的 Base64,至少 32 字节 |
XHIM_POLICY_WEBHOOK_REQUEST_TIMEOUT | 同步策略最大等待时间,默认 5 秒,最大 30 秒 |
XHIM_POLICY_WEBHOOK_FAILURE_MODE | deny 为失败关闭,allow 为失败放行 |
XHIM_POLICY_WEBHOOK_EVENTS | 可选的逗号分隔 before-event 白名单;空值表示全部 |
XHIM_RTC_PROVIDER | 返回给客户端的 RTC Provider 标识 |
XHIM_RTC_ENDPOINT | 安全 wss:// 或 https:// RTC Endpoint |
XHIM_RTC_SIGNING_KEY_ID | RTC Token 签名 Key ID,用于轮换 |
XHIM_RTC_TOKEN_KEY_BASE64 | 独立 RTC HMAC Key;生产至少 32 字节 |
XHIM_RTC_TOKEN_LIFETIME | 用户级 RTC Token 有效期,最大 1 小时 |
完整默认值见 .env.example。
PostgreSQL 备份、恢复演练与滚动升级
生产 PostgreSQL 运维工具位于 scripts/postgres,提供:
pg_dumpcustom format 备份、canonical 元数据、SHA-256 和pg_restore --list离线校验;- Hash、TOC 和实际导入共用匿名固定 inode/FD 的单事务恢复,以及 canonical 恢复演练报告;
- 迁移前备份身份校验、N/N-1 兼容窗口和应用回滚前置门禁。
工具不接收数据库 URL 命令行参数;源库和恢复库分别只从 XHIM_POSTGRES_SOURCE_URL、XHIM_POSTGRES_RESTORE_URL 读取。它们不会 自动创建/删除数据库,不使用 pg_restore --clean/--create,也不会执行向后 Schema Migration。正式滚动顺序、受控非空目标例外以及“只回应用、不盲回 Schema”的处理流程见上述运维文档。
任何服务端源码、配置、Migration 或公开接口变化都必须按 Server 发布审计门禁完成 clean commit、一次性 Continuity Bootstrap/连续链校验、配置与 PostgreSQL 加密备份、Migration Tree 与在线 Schema 核验、隔离恢复演练、N/N-1 兼容证据、不可变 Image Digest、 版本/健康/注册登录 Postflight,以及 HTTP、WSS 和目标端 SDK 验收。
该门禁只审计证据,不会自动 SSH、迁移、重启或部署服务器。CI 绿色或本地测试 通过不代表服务端已部署;没有目标主机 Runtime Observation 和真实 Postflight 时只能标记“代码完成,服务端未部署”。发布策略的关键边界是:
- Deployment Manifest 必须指定不可覆盖的
continuity_anchor_file,并显式设置development_phone_auth_required。Development Demo 必须为true且自动 完成注册/密码登录探针;Production 必须为false; - Continuity Bootstrap 只允许在首次纳管时执行一次。锚点存在、Fingerprint Key 或持久身份变化后都不能重建基线;
- Production 的
XHIM_SERVER_IMAGE必须使用registry/repository@sha256:<digest>,Runtime RepoDigests 必须非空且匹配; - Runtime Observation 会读取在线
xhim_schema_migrations完整有序集合,并 记录server、postgres、gateway三个 Service 的 Container ID、Image ID/Reference/RepoDigests、唯一 Compose Network 和完整 Mount;不会把容器 环境变量或业务数据写入证据; - 普通
server-and-config只允许修改请求/连接限额、关闭超时、Endpoint Lifetime、最低协议版本、多端登录策略、阅后即焚 Worker 参数、媒体授权时长、 Push/Webhook Worker 参数和 RTC Token Lifetime。精确变量名以 发布审计门禁中的完整 Allowlist为准;身份、Secret、 域名、数据库、对象存储、环境类型和 Development Login 一律不在普通 Allowlist; - Migration 发布必须绑定完整 Migration Tree、发布前后在线 Schema、实际备份 Bundle、真实加密制品及验证报告、隔离恢复演练报告;兼容声明只声明窗口,不能 替代 Release N/N-1 在目标 Schema 上的实际双向运行报告;
server-only只重建 Server,且 PostgreSQL/Gateway 容器及 Image、Compose Network 和 Mount 必须不变;Server 也不能附加额外 Host Bind 或外部 Network。server-and-config回滚必须在部署锁内用同一个版本化 Config Set 原子恢复.env、Compose、deploy/Caddyfile和.env.release,禁止部分回滚;- Preflight 还必须读取 Protected CI/运维生成的
0600candidate.env.release、HMAC 签名 Image Provenance 和 HMAC 签名 Migration Readiness;三者分别绑定候选 Release、Commit/Server Tree/Image Digest/Image ID/SBOM/镜像签名,以及在线 Schema/候选 Migration Tree/加密 备份/Restore Drill/N/N-1; - Postflight 会使用真实 Bearer Token 执行公开 WSS 的已认证 Upgrade: Development 使用自动注册密码登录返回的 Token,Test/Production 必须通过
--websocket-probe-token-file注入0600短期 Token。完整 Ping/Pong、消息 收发、ACK、已读、同步和重连仍需两个账号在目标端正式 SDK 制品上回归。
Push Provider 模式
默认没有 Push 配置时,XHIM_PUSH_MODE=disabled,Worker 不会启动。原有部署只要 配置 XHIM_PUSH_WEBHOOK_URL,仍会自动进入 webhook 模式并将所有平台交给 同一个聚合服务。
自建直连使用:
dotenv
XHIM_PUSH_MODE=direct
# APNs:三项必须同时配置;Topic 可留空并使用登记设备的 App ID。
XHIM_PUSH_APNS_TEAM_ID=YOUR_TEAM_ID
XHIM_PUSH_APNS_KEY_ID=YOUR_KEY_ID
XHIM_PUSH_APNS_PRIVATE_KEY_BASE64=<AuthKey_xxx.p8 的 Base64>
XHIM_PUSH_APNS_TOPIC=com.example.chat
# FCM:完整 service-account.json 的 Base64,不是客户端 google-services.json。
XHIM_PUSH_FCM_SERVICE_ACCOUNT_JSON_BASE64=<service-account.json 的 Base64>
# Huawei Push Kit:三项必须同时配置。
XHIM_PUSH_HUAWEI_APP_ID=YOUR_APP_ID
XHIM_PUSH_HUAWEI_CLIENT_ID=YOUR_OAUTH_CLIENT_ID
XHIM_PUSH_HUAWEI_CLIENT_SECRET=YOUR_OAUTH_CLIENT_SECRETDirect 模式按设备登记的 platform 严格路由:
apns使用 Apple ES256 Token Auth 和 Go 标准 HTTP Client 的 TLS/HTTP/2 连接;environment=development|sandbox走 Sandbox,production|prod|release或空值走 Production;fcm使用服务账号 RS256 JWT 换取短期 OAuth2 Token,再调用 FCM HTTP v1;huawei使用client_credentials换取短期 Token,再调用 Push Kit v1;web只有同时配置 Webhook URL 才可用,否则该平台明确返回provider_not_configured:web,不会伪造投递成功。
各 Provider 会缓存并在到期前更新短期凭证。网络错误、429、5xx 和 Provider 明确的临时错误进入耐久 Outbox 重试,并支持秒数或 HTTP-date 格式的 Retry-After;APNs Unregistered/ExpiredToken、FCM UNREGISTERED、Huawei 80300007 等令牌失效响应会通过结构化 InvalidToken 结果自动停用对应设备。 停用操作同时校验 App、账号、设备、平台和原 Token,防止请求在途期间客户端 刷新 Token 后被旧响应误停用;状态变更与不含 Token 的安全审计原子提交,重复 响应为幂等 no-op。配置缺失、部分配置和认证错误均 fail-closed。进程日志只记录 模式和已启用的平台,不记录私钥、服务账号 JSON、OAuth Secret、访问 Token 或 设备 Token。
Push 注册容量按 app_id + account_id + user_id 计算,启用和已禁用记录都占用 配额,避免通过反复注册/禁用持续扩张数据库。注册一个新的 device_id 到达上限 时,存储事务会先确定性淘汰已禁用设备,再按 updated_at、created_at、 device_id 淘汰最久未更新的设备;同一设备刷新 Token 不触发淘汰。Memory 和 PostgreSQL Adapter 使用同一策略,PostgreSQL 通过锁定用户行串行化同账号并发 注册。客户端注册响应同时通过 Protobuf 字段 device_limit、 evicted_count 和兼容响应头返回容量结果。相同 Provider Token 换账号或 设备时会在同一事务迁移所有权,并通过 token_reassigned=true 明确返回,避免 唯一索引冲突或旧账号继续收到 Push。响应、日志与 push.devices_evicted 审计都不包含原始 Device Token。
投递查询始终携带同一硬上限,SQL 使用 LIMIT,Worker 只创建 XHIM_PUSH_DELIVERY_CONCURRENCY 个有界消费者,不会按设备数量无限创建 goroutine。降低设备上限后,下一次新设备注册会在同一事务清理历史超额记录; 正式调整前应先观察 push.devices_evicted 审计数量。
生产环境应通过容器 Secret、Kubernetes Secret 或 Vault 注入这些值,不要提交到 Git。FCM Provider 不使用服务账号 JSON 中可被篡改的 token_uri,而固定调用 Google 官方 OAuth 与 FCM 地址;APNs 和 Huawei 的生产地址同样固定在代码中。
耐久业务事件 Webhook
业务 Webhook 与 Push Webhook 是两套完全独立的协议。业务方法只在原业务 PostgreSQL 事务内追加一条 xhim_webhook_outbox 记录;事务提交前不会访问外部 网络。提交成功后独立 Worker 才租约并投递,因此接收方变慢、宕机或返回错误不会 占用业务事务,也不会造成“业务回滚但 Webhook 已发出”。Memory Adapter 保持相同 状态机语义,但只用于开发和测试;生产重启恢复依赖 PostgreSQL。
启用配置示例:
dotenv
XHIM_WEBHOOK_URL=https://events.example.com/xhim
XHIM_WEBHOOK_KEY_ID=xhim-business-2026-01
XHIM_WEBHOOK_SECRET_BASE64=<至少 32 字节随机 Key 的 Base64>URL、Key ID、Secret 必须三项同时配置。Secret 太短、配置一半、URL 内含账号密码、 Query/Fragment,或生产环境使用 HTTP,服务都会拒绝启动。开发环境只有同时设置 XHIM_ALLOW_INSECURE_DEVELOPMENT_TRANSPORT=true 才允许 HTTP。Secret、消息 Body、Token 和完整 Envelope 不写普通日志,也不由管理列表返回。
每次请求体是稳定的 1.0 JSON Envelope:
json
{
"version": "1.0",
"event_id": "whevt_...",
"app_id": "com.example.chat",
"type": "message.sent",
"occurred_at": "2026-07-26T12:00:00Z",
"resource": {
"type": "message",
"id": "msg_...",
"conversation_id": "conv_...",
"content_type": "text/plain",
"content_version": 1,
"server_sequence": 42
},
"actor": {
"user_id": "alice",
"account_id": "account-alice"
},
"trace": {
"trace_id": "trace-...",
"operation_id": "operation-...",
"client_id": "client-message-..."
}
}当前 after-event 至少包含:
message.sent、message.mutated、message.history.imported、profile.updated;friend.requested、friend.resolved、friend.deleted、friendships.imported;group.created、group.members.changed、group.governance.changed、group.left、group.dismissed;- 额外包含
group.join.requested、group.join.resolved和block.changed; - Push 设备状态包含
push.device.registered、push.device.disabled; - 用户聚合在线状态变化包含
presence.changed。续租心跳、TTL 延长和未改变 聚合状态的平台内刷新不会产生业务 Webhook。
消息事件只携带定位、类型、版本、序列和修订元数据,不复制消息 Payload 或 Fallback Text。接收方如果需要业务内容,应使用 resource.id 通过自己的受控 服务端权限查询。 历史导入只产生一个聚合 message.history.imported,不为每条导入消息 生成 message.sent,也不在聚合信封中放入正文、fallback、sender 列表或 source message ID。
Push 业务事件只包含用户 ID、设备 ID、平台、启用状态和容量结果;不会包含 Provider Token、Environment、Locale 或 Provider 原始失败正文。presence.changed 只包含用户 ID、online|away|offline、已排序的平台集合和单调 transient sequence; 不会包含 Account ID、Session ID、租约到期时间或心跳参数。设备失效的原始 Provider Code 只保留在受控审计中,不进入业务 Webhook。
签名与防重放 Headers:
text
X-XHIM-Webhook-Version: v1
X-XHIM-Webhook-Key-ID: <key id>
X-XHIM-Webhook-Timestamp: <Unix seconds>
X-XHIM-Webhook-Signature: v1=<hex HMAC-SHA256(secret, timestamp + "." + rawBody)>
X-XHIM-Event-ID: <event_id>
X-XHIM-Delivery-ID: <event_id>
Idempotency-Key: <event_id>接收方必须在解析 JSON 前用收到的原始 Body 做常量时间签名校验,按 Key ID 选择 轮换中的密钥,并拒绝与本机时间相差超过 5 分钟的 Timestamp。event_id 在业务 事件生命周期内稳定;接收方应按它做唯一约束,即使响应丢失导致重复投递也只执行 一次。轮换密钥时先让接收方同时接受旧/新 Key,再切换发送端 Key ID,至少保留旧 Key 一个重放窗口加最长在途时间。
2xx 表示成功;除 429 外的 4xx 进入 Dead Letter;429、5xx 和网络错误 重试。Retry-After 同时支持秒数和 HTTP-date,并有 24 小时安全上限;没有该 Header 时使用有限指数 Full Jitter。每次 Claim 都增加 attempt 和 lease_revision,旧 Worker 无法覆盖重启回收或另一个 Worker 的新尝试。达到最大 尝试次数后进入 Dead Letter。
管理接口:
GET /v1/admin/webhooks/deliveries:按app_id、status分页查看安全元数据;POST /v1/admin/webhooks/deliveries:retry:仅允许重试 Dead Letter,并在同一 事务写入webhook.delivery.retry审计;/metrics:暴露xhim_webhook_pending、xhim_webhook_leased、xhim_webhook_delivered、xhim_webhook_dead_letter和xhim_webhook_retry_due。
Enterprise 前置策略 Webhook
前置策略与上述事后通知不是同一个 Worker 或协议。它在消息、好友申请、建群、 群成员、入群申请和群治理提交前同步执行,可以拒绝操作或改写明确白名单字段; 所有 Patch 都会重新执行领域与权限校验。启用它需要 enterprise runtime 和包含 integration.webhook.before 的授权。请求签名、响应、事件清单及失败模式见 前置策略与事后业务 Webhook。产品版本与授权 说明见 XHIM 产品页。
客户业务后台无需伪装成移动端用户或先签发短期 Token,可使用 服务端到服务端 REST API安全地创建用户、 批量读取受隐私保护的资料、检查关系、幂等创建直聊、管理群组和代发消息。 这组路由与 Admin Key 隔离,且继续复用同一 Application Service 的权限、策略、幂等、推送与同步链路。
五端 SDK 的一键发现还使用:
| 环境变量 | 用途 |
|---|---|
XHIM_DEFAULT_APP_ID | /v1/sdk/config 返回的默认租户 App ID |
XHIM_ENABLE_DEVELOPMENT_LOGIN | 仅 Development 可开启的测试免密登录、手机号注册和密码登录;Production 启动时会拒绝 |
XHIM_E2EE_ENABLED | true 时服务端强制 MLS,false 时服务端拒绝 MLS 信封;App 无权改动 |
XHIM_SMS_MODE | disabled 保留本地密码流程;webhook 开启真实短信验证并要求完整签名配置 |
XHIM_SMS_WEBHOOK_URL | 服务端内部短信适配器 URL;生产网络必须 HTTPS |
XHIM_MINIMUM_CLIENT_PROTOCOL_VERSION | 最低客户端协议版本,默认 1,不得大于当前服务端协议版本 |
管理 API
管理 API 只用于你的业务服务端调用,不允许 App 持有 Admin Key。
自托管管理后台
启动 Server 后访问:
text
https://<你的 XHIM 域名>/admin/Development 局域网环境也可访问 http://<Mac局域网IP>:18080/admin/。用 XHIM_ADMIN_USERNAME 和对应的 密码登录后可以:
- 查看服务状态、版本、媒体存储和公开 Endpoint;
- 复制 SDK 所需的 Bootstrap URL、Endpoint Key ID 和 Ed25519 公钥;
- 分页查看/创建用户和单聊/群聊会话;
- 按 App + 用户/账号精确查看登录设备,并在二次确认后幂等撤销异常会话;
- 按账号分页查询 Push 注册、查看失败状态并幂等禁用异常设备;
- 通过管理 API 查询业务 Webhook 投递状态并审计重试 Dead Letter;
- 为 Alice/Bob 等联调账号按设备 ID 和客户端平台签发短期测试 Token,并显示 session ID 与本次策略淘汰数量;
- 查看群治理、成员和拉黑等安全审计记录。
- 在“账号安全”中校验当前密码并同时修改管理员账号和密码;新凭据只以 bcrypt cost 12 哈希持久化,保存后当前浏览器自动续签,其他管理会话立即失效。
五端 SDK Easy Login 使用两个公开、无管理权限的端点:
text
GET /v1/sdk/config
POST /v1/sdk/development:login/v1/sdk/config 只返回 Schema/Server 版本、服务端协议版本、最低客户端协议 版本、固定排序能力列表、环境、默认 App ID、Endpoint Key ID、Ed25519 公钥、 Easy Login 和手机号 Demo 认证是否可用,不返回 Admin Key、服务端签名私钥或 业务用户 Token。 该公开快照与认证成功响应来自同一个服务端能力对象,SDK 可在申请用户凭证前先 校验版本和能力,避免登录后才发现不兼容。/v1/sdk/development:login 只有在 XHIM_SERVER_ENV=development 且 XHIM_ENABLE_DEVELOPMENT_LOGIN=true 时存在,只为已创建用户签发一小时开发 凭证;Production 配置会在进程启动前拒绝该开关,HTTP 层也会再次校验环境并 fail-closed,不会把生产免密登录暴露给客户端。
Easy Login 只接受 application/json,请求体硬上限为 4 KiB;成功配置响应和 凭证响应分别限制在 8 KiB、16 KiB。两个端点的成功、失败、404 和 429 响应均为 Cache-Control: no-store。免密登录除全局入口限流外,还按可信客户端 IP 使用独立的单进程令牌桶(每秒 1 次、突发 5 次);多副本或可能被非受信网络 访问的 Development 环境仍应在反向代理设置更严格的集中式限流。访问日志只记录 方法、路由、状态、耗时和 Request ID,不记录请求体、用户 Token 或 Admin Key。
普通网页使用独立的 @xihansoftware/xhim-web 包。HTTP API 接受浏览器 CORS 预检;实时连接因浏览器不能自定义 WebSocket Authorization Header,会在 Sec-WebSocket-Protocol 中同时发送 xhim.v1 和 xhim.bearer.<base64url-token>。服务端只协商并回显 xhim.v1,不会把凭证 子协议反射回页面。反向代理、WAF 和 APM 必须允许该 Header,并像 Authorization 一样脱敏,禁止写入访问日志。生产只允许 HTTPS/WSS,且通过 XHIM_ALLOWED_ORIGINS 限制审核过的网页来源。
浏览器不再接触 Admin Key,也不保存明文密码。登录成功后服务端签发 HttpOnly + SameSite=Strict 会话 Cookie,写操作还必须同时通过同源 Origin 和 CSRF Token 校验。通过“账号安全”修改凭据后,PostgreSQL 中的持久化 bcrypt 哈希会覆盖首次部署的环境变量引导凭据;各实例在验证管理请求前刷新凭据, 因此多副本也会使旧会话失效。Session Key 仍由 Secret Manager 或环境变量管理, 不会写入数据库。登录失败按客户端 IP 独立限流。Production 仍应由 VPN/零信任网关 或独立管理域名限制 /admin/ 和 /v1/admin/* 的访问;页面不能代替业务 系统的 RBAC。服务端域名、IP、WSS、上传、下载、S3 和 RTC Provider 均由 环境变量配置,客户可完全自主部署。
新安装执行 go run ./cmd/xhim-keygen 时,环境变量输出会包含首次引导用的管理员 账号、bcrypt 哈希和 Session Key,终端另外只显示一次初始密码。服务启动后应在 管理后台“账号安全”中完成首次凭据轮换。无法进入管理后台的灾难恢复场景,才使用 下面的离线方式生成哈希并更新环境变量;如数据库中已有持久化管理凭据,需要由 数据库管理员在受控维护窗口清除 xhim_admin_credential 后重启,不能只改环境变量:
bash
read -r -s XHIM_NEW_ADMIN_PASSWORD
printf '%s' "$XHIM_NEW_ADMIN_PASSWORD" | go run ./cmd/xhim-admin-password
unset XHIM_NEW_ADMIN_PASSWORD将输出值直接写入 XHIM_ADMIN_PASSWORD_BCRYPT_BASE64 并重启 Server。明文密码只保存 在管理员的密码管理器中。
text
POST /v1/admin/session:login
GET /v1/admin/session
POST /v1/admin/session:logout
POST /v1/admin/credentials:changetext
GET /v1/admin/overview
GET /v1/admin/users
GET /v1/admin/conversations
GET /v1/admin/sessions
POST /v1/admin/users:upsert
POST /v1/admin/conversations:create
POST /v1/admin/tokens:issue
POST /v1/admin/sessions:revoke
GET /v1/admin/audit
GET /v1/admin/push/devices
POST /v1/admin/push/devices:disable/v1/admin/users:upsert 与 /v1/server/users:upsert 可选接受 application_extension_json;响应和用户列表会返回服务端规范化后的 权威 JSON。该字段对精确陌生人查询也公开,只能用于非敏感业务标签/ 开关;严禁存放个人敏感信息、密码、Token、会话/认证状态或加密密钥。
设备会话查询必须传 app_id,并至少传一个 user_id 或 account_id;两者 同时传入时必须解析为同一租户内的同一用户。未找到或两者不匹配时返回 200 空列表,不会把列表搜索误报成 404,也不会退化为模糊查询或跨租户扫描。 默认返回 50 条,最多 100 条,活跃会话优先,再按创建时间 倒序;truncated=true 表示存在未返回的历史记录:
text
GET /v1/admin/sessions?app_id=<app>&user_id=<user>&limit=100
GET /v1/admin/sessions?app_id=<app>&account_id=<account>&limit=100响应只包含 App/用户/账号、稳定 session_id、device_id、平台、 active|revoked|expired 状态和创建、最后活跃、过期、撤销时间及撤销原因。 管理端没有可信的“相对当前设备”,因此不会返回或伪造 current;只有用户本人 通过 /v1/sessions:list 查询时才有 principal-relative current。管理 API 和页面都不返回 Access Token、JWT、内部 token_id、Admin Key 或签名密钥。
查询响应固定限制在 2 MiB 以内。撤销请求只接受最大 8 KiB 的严格 JSON,未知 字段会被拒绝,且必须显式传 confirmed=true:
json
{
"app_id": "com.example.chat",
"user_id": "alice",
"account_id": "account-alice",
"session_id": "session-id-from-admin-list",
"mutation_id": "stable-admin-operation-id",
"reason": "compromised device",
"confirmed": true
}mutation_id 必须在调用方重试期间保持不变。完全相同的重试返回第一次结果并 设置 idempotent_replay=true;改变目标或原因复用同一个 mutation ID 返回 409 idempotency_conflict。首次实际撤销与 session.revoked_by_admin 审计在同一 Memory 锁域或 PostgreSQL 事务中提交, 随后向目标 WSS 发送 SESSION_REVOKED,并清理该 session 的 Presence/Typing 租约。审计 Details 只包含设备、平台、管理控制面 actor 和原因,不包含任何 Token。普通 Bearer Token 不能访问这两个管理端点;普通 /v1/sessions:list、/v1/sessions:revoke 仍严格绑定自己的 App + 账号 + 用户, 不能通过请求参数指定其他用户。
Push 设备查询必须同时传 app_id 和 account_id,使用 after_registration_id 做稳定 keyset 分页,默认 50 条、每页最多 100 条:
text
GET /v1/admin/push/devices?app_id=<app>&account_id=<account>&limit=50响应固定使用 schema_version=1,包含稳定 registration_id、用户、平台、环境、 启用状态、更新时间以及 failure_status。token_fingerprint 是 SHA-256 的不可逆短指纹,只用于客服排障关联;原始 Device Token 不会出现在 管理 API、管理页面或管理员禁用审计中。
禁用设备请求为严格 JSON:
json
{
"app_id": "com.example.chat",
"account_id": "account-alice",
"registration_id": "alice-iphone"
}同一注册重复禁用返回成功且 changed=false,不会重复写审计;首次从启用变为 禁用时,设备状态和 push.device_disabled_by_admin 审计在同一存储事务中提交。 registration_id 当前对应 SDK 注册时提供的稳定 device_id,其命名空间是 app_id + account_id。两个端点均要求 X-XHIM-Admin-Key,并返回 Cache-Control: no-store。
客户端 API:
text
POST /v1/session:authenticate application/x-protobuf
POST /v1/sessions:list application/x-protobuf
POST /v1/sessions:revoke application/x-protobuf
POST /v1/users/profile:get application/x-protobuf
POST /v1/users/profiles:get application/x-protobuf
POST /v1/users/phone:resolve application/x-protobuf
POST /v1/users/profile:update application/x-protobuf
POST /v1/conversations/direct:getOrCreate application/x-protobuf
POST /v1/messages:send application/x-protobuf
POST /v1/messages:mutate application/x-protobuf
POST /v1/messages/history:get application/x-protobuf
POST /v1/messages:deleteForSelf application/x-protobuf
POST /v1/conversations:clear application/x-protobuf
POST /v1/conversations:hide application/x-protobuf
POST /v1/conversations:hideAll application/x-protobuf
POST /v1/conversations:markRead application/x-protobuf
POST /v1/conversations:markAllRead application/x-protobuf
POST /v1/conversations:setPreference application/x-protobuf
POST /v1/sync application/x-protobuf
POST /v1/presence:publish application/x-protobuf
POST /v1/presence:query application/x-protobuf
POST /v1/typing:publish application/x-protobuf
POST /v1/social/friend-requests:send application/x-protobuf
POST /v1/social/friend-requests:resolve application/x-protobuf
POST /v1/social/friendships:delete application/x-protobuf
POST /v1/social/friendships:setRemark application/x-protobuf
POST /v1/social/friendships:update application/x-protobuf
POST /v1/relationships:check application/x-protobuf
POST /v1/social/snapshot:get application/x-protobuf
POST /v1/social/groups:create application/x-protobuf
POST /v1/social/group-members:change application/x-protobuf
POST /v1/social/group-members:list application/x-protobuf
POST /v1/social/groups:leave application/x-protobuf
POST /v1/social/groups:dismiss application/x-protobuf
POST /v1/social/blocks:set application/x-protobuf
POST /v1/social/group-joins:request application/x-protobuf
POST /v1/social/group-joins:resolve application/x-protobuf
POST /v1/social/groups:govern application/x-protobuf
POST /v1/push/devices:register application/x-protobuf
POST /v1/push/devices:disable application/x-protobuf
POST /v1/calls:invite application/x-protobuf
POST /v1/calls:accept application/x-protobuf
POST /v1/calls:reject application/x-protobuf
POST /v1/calls:end application/x-protobuf
POST /v1/calls/signals:list application/x-protobuf
POST /v1/media/uploads:prepare application/x-protobuf
POST /v1/media/uploads:complete application/x-protobuf
POST /v1/media/downloads:authorize application/x-protobuf
POST /v1/users/avatar/uploads:prepare application/json
POST /v1/users/avatar/uploads:complete application/json
GET /v1/users/avatars/{mediaID}?app_id=... public stable avatar redirect
GET /v1/realtime WebSocket, subprotocol xhim.v1/v1/social/group-members:list 在权限校验后、稳定 user_id 游标分页前 原子应用可选 query / exact_id_match / joined_from_ms (包含) / joined_before_ms(不包含) / role_mask / excluded_user_ids。 role_mask 的 owner/admin/member 位分别为 1/2/4;排除账号最多 100 个且不得重复。无筛选的全量分页供 Core 重建群成员投影, 带筛选的页不得被标记为完整投影。 高级筛选请求必须传 filter_contract_version=1,服务端只在真正执行 筛选后回显版本 1;因此新 SDK 连到会忽略未知 Protobuf 字段的 旧服务端时会明确返回 unsupported,不会把未筛选页当成正确结果。
头像上传路由是标准 Server 合同。升级后可用无效/缺失 Bearer Token 探测 POST /v1/users/avatar/uploads:prepare:401/403 表示路由已加载,404 表示 当前进程仍是旧制品或反向代理未指向新版本,必须重新部署并重启,不能要求客户端 改用对象存储直传。
用户资料接口只接受当前 Bearer Token 所属租户:单用户查询不会暴露 account_id,批量查询最多 100 个去重 user_id,缺失用户在 missing_user_ids 中按请求顺序返回。资料更新只能修改当前用户,并通过 Protobuf optional 字段区分“不修改”和“清空”;重复提交同一最终状态不会 推进 updated_at。
所有新的 UserProfile 回执(当前资料、批量、手机号精确解析、单聊 对端资料和 UserProfileUpsert Sync)都显式写入 additive user_type = 17:HUMAN=1、NOTIFICATION_SERVICE=2。陌生人脱敏 只移除私密资料,不移除这个公开账号分类;服务端不根据 user ID 前缀推断类型。
/v1/conversations/direct:getOrCreate 不接受客户端指定发起人或 App ID,只以 Token 用户和 peer_user_id 创建单聊。服务端先复用历史同成员单聊;首次创建时 按排序后的两个用户 ID 生成确定性 opaque ID,并在 Memory/PostgreSQL 中串行化 同一用户对的并发请求。双方反向调用会得到同一 conversation_id。接口返回 对端最小资料,消息发送阶段仍会执行双向黑名单校验。首次创建会在同一事务中向 双方账号流追加可跳过、版本化的 ConversationUpsert,因此同一用户的其他设备 无需轮询即可建立会话投影。当前用户资料实际变更时也会向自己的账号流追加 UserProfileUpsert;资料无变化时不追加重复事件。
/v1/messages/history:get 是服务端权威历史入口,结果按 server_seq 从新到旧 返回。before_server_seq=0 从当前高水位开始;下一页把响应中的 next_before_server_seq 原样传回,服务端使用严格 < 游标避免重复。默认每页 50 条、最多 200 条。查询要求 Token 用户仍是会话成员,并自动过滤该用户已执行 “仅为我删除”的消息以及 server_seq <= cleared_through_server_seq 的消息; 这些私有状态不会影响其他成员。响应中的 latest_server_seq 与历史页来自同一 数据库快照。
/v1/messages:deleteForSelf 只写当前用户的消息可见性,不修改共享消息; /v1/messages:mutate 的 Recall 才是面向所有成员的全局消息撤回。清空接口 /v1/conversations:clear 记录客户端明确提交的 through_server_seq,不得超过 会话最新序号;隐藏接口 /v1/conversations:hide 的高水位完全由服务端在事务内 快照当前最新序号,客户端不能伪造。三类写操作均要求调用方提供稳定 mutation_id 和精确 expected_revision:完全相同的重试返回原结果,不同载荷 复用 mutation ID 或 revision 过期返回冲突。成功变更只进入当前用户的账号事件 流,事件 Schema v2 且 skippable=true,旧客户端可安全跳过;新消息序号高于 hidden_through_server_seq 时,会话自然重新显示,无需服务端重置布尔值。
/v1/conversations:markRead 使用用户 Bearer Token,不接受客户端传入 user ID。 服务端校验用户仍是会话成员、read_server_sequence > 0 且不超过该会话最新 消息序号,再在同一事务中单调更新 conversation_reads(app_id, conversation_id, user_id)。首次 revision 为 1; 调用方可传 optional expected_revision 做精确 CAS,未传时兼容旧客户端的纯 单调更新。同值或低值即使带旧 revision 也按幂等读取返回当前状态,不追加重复 事件;提高已读序号时 revision 不匹配则返回冲突。
一次成功提高会原子追加两类账号事件:当前账号收到不可跳过的 ConversationReadUpsert,用于同账号多设备同步;会话内其他当前成员收到 Schema v2、skippable=true 的 ConversationPeerReadUpsert,其中包含 reader_user_id、已读 server_seq 和 revision。客户端按 conversation_id + reader_user_id 覆盖投影即可得到单聊或群聊的成员已读状态; 已退出成员和会话外账号不会收到。已读事件不会触发离线 Push。
标准 xhim.message.mention@1 的 Push 策略例外于普通会话免打扰:Server 在消息 写入前校验并解析 mentioned_user_ids / mention_all,然后只为实际被提醒的接收 成员在内部 Push Outbox 写入 bypass_conversation_mute=true。Push Worker 只信任 这个服务端派生字段,不接受客户端直接设置绕过标记。被 @ 的成员会收到该条 通知;同群未被 @ 的成员仍遵守 notifications_muted。该例外不改变聊天记录、 未读水位或权限校验,也不会让退出群聊的账号重新获得消息。
/v1/conversations:markAllRead 是独立的原子存储操作,不会循环调用上述单会话 接口。服务端只从 Bearer Token 取 App、账号和用户身份,在 Memory 的单一锁域或 PostgreSQL SERIALIZABLE 事务中锁定该用户当前有权限的全部会话,以每个会话在 该次调用快照中的最新 server_seq 批量提高 watermark。发送与批量已读并发时, 结果严格对应其中一个可串行化顺序;后到的新消息保留未读,不会被调用开始后 猜测性清零。会话数为 0 也会成功写入幂等回执。
请求必须提供稳定 mutation_id。完全相同的重试返回第一次提交的 changed_conversation_count、scheduled_burn_count 和明细,并设置 idempotent_replay=true;同一用户复用 mutation ID 但改变 max_changed_reads 会返回幂等冲突。max_changed_reads=0 不返回逐会话明细, 1–100 最多返回对应数量,超过 100 会拒绝;完整变更计数始终返回, changed_reads_truncated 明确表示明细是否被截断,避免大账号产生巨大响应。 每个实际提高的会话仍在同一事务中增加独立单调 revision:当前账号收到不可 跳过的 ConversationReadUpsert,其他当前成员收到可跳过的 ConversationPeerReadUpsert。所有被 watermark 首次跨过的阅后即焚接收状态也 在同一事务中写入 read_crossed_at 和 burn_due_at,不会出现“已读成功但焚毁 未安排”的部分提交。
/v1/conversations:hideAll 同样是账号级原子操作,不是由客户端逐个调用 /v1/conversations:hide。PostgreSQL 使用 SERIALIZABLE 事务和用户级 advisory lock,把每个非空会话的 hidden_through_server_sequence 提高到各自当前最新 序列,写入逐会话 ConversationViewUpsert、审计记录和 mutation 回执。响应只 返回 changed_conversation_count 与 idempotent_replay,避免大账号形成无界 响应;客户端从 Sync 更新本地投影。后续消息序列高于隐藏水位时,该会话自然 重新出现。空会话不生成无意义视图行,事务失败不会留下部分隐藏状态。
权威阅后即焚
发送端可在不可变 SendMessageRequest 中附带生命周期策略;字段缺失、 UNSPECIFIED 或显式 DURABLE 都按普通永久消息处理,因而旧客户端完全兼容:
textproto
lifecycle_policy {
kind: MESSAGE_LIFECYCLE_KIND_BURN_AFTER_READ
burn_after_read_ms: 30000
}burn_after_read_ms 是接收者读到消息之后的延时,而不是由发送设备决定的绝对 到期时间。硬边界为 1 秒至 30 天,部署默认再限制为最多 7 天;超出 XHIM_MAX_BURN_AFTER_READ 的发送会在持久化前被拒绝。生命周期属于消息不可变 幂等载荷:相同 client_message_id 改变策略或延时会返回幂等冲突,不能借重试 改变已发送消息的销毁语义。
服务端为每个接收成员分别保存状态,发送者不创建焚毁状态。某个接收者的已读 watermark 第一次跨过消息 server_seq 时,服务端以自己的时钟原子写入且永久 冻结 read_crossed_at 和 burn_due_at;重复、降序、乱序或并发已读不会延后 期限。群聊成员各自计时,一个成员到期不影响发送者或其他成员。成员退出会立即 失去历史、同步、Push 和媒体访问权限,并由 worker 把其未完成状态收敛为私有 删除。Recall 仍是独立的全员永久撤回,不会被私有焚毁替代。
到达 burn_due_at 的精确边界后,即使后台 worker 尚未处理, /v1/messages/history:get、/v1/sync、Push Claim 和媒体下载授权也会立即按 当前用户过滤正文。Sync 仍推进原始账号 Cursor,避免被不可见事件卡住。阅后即焚 Push 从创建时就只包含通用提示,不携带 fallback_text 或 Payload。图片、语音、 视频和文件的服务端授权会跟随内置 xhim.media.image/audio/video/file@1 Payload 中的 MediaRef;自定义消息若引用受保护资源,业务扩展必须实现同等的 授权策略,不能只依赖不可猜测 URL。
后台 worker 使用耐久 PostgreSQL 状态、有限批次和 FOR UPDATE ... SKIP LOCKED,支持进程重启和多副本并行;可见性行、私有 MessageVisibilityUpsert、message.burn_after_read.expired 安全审计和完成 状态在同一事务提交。提交后再按 App/账号发布跨实例 Sync Hint,提示在线设备 拉取事件,Hint 本身不含消息正文。worker 与手工“仅为我删除”共用消息可见性锁, 无论先后或并发最终都只有 revision 1,不会产生第二次删除。
相关部署参数:
| 环境变量 | 默认值 | 约束/用途 |
|---|---|---|
XHIM_MAX_BURN_AFTER_READ | 168h | 租户发送上限,必须在 1s 至 720h 内 |
XHIM_BURN_WORKER_BATCH_SIZE | 100 | 单批最多 500 条,限制锁和事务规模 |
XHIM_BURN_WORKER_IDLE_INTERVAL | 1s | 空闲轮询间隔,必须大于 0 且不超过 1 分钟 |
XHIM_CONVERSATION_RETENTION_WORKER_BATCH_SIZE | 100 | 单批推进账号私有过期水位的会话数,最大 500 |
XHIM_CONVERSATION_RETENTION_WORKER_IDLE_INTERVAL | 30s | 无到期会话时的轮询间隔,必须大于 0 且不超过 1 分钟 |
/metrics 暴露 xhim_burn_after_read_waiting、xhim_burn_after_read_scheduled、 xhim_burn_after_read_due、xhim_burn_after_read_burned 和 xhim_burn_after_read_metrics_available。生产应对 due 持续增长或 metrics_available=0 告警,并结合审计确认 worker 是否正常收敛。
/v1/session:authenticate 返回当前 DeviceSession、登录策略、容量,以及 server_protocol_version、minimum_client_protocol_version、 server_version 和固定、排序、有界的 capabilities。当前服务端协议为 v2; 请求不带新增字段或显式传 client_protocol_version=0 时严格按 legacy v1 处理。默认最低版本保持 v1,因此现有客户端不会因能力协商上线而中断。部署方 只有在所有活跃 SDK 已发送 v2 后,才应把 XHIM_MINIMUM_CLIENT_PROTOCOL_VERSION 提升到 2;配置为 0、非数字或大于 服务端协议版本会导致服务启动失败。
新 SDK 可同时发送可读的 client_sdk_version 和最多 32 个 required_capabilities。缺少必需能力返回 HTTP 422、稳定错误码 unsupported_capability;客户端协议低于最低版本返回 HTTP 426、稳定错误码 client_upgrade_required;高于服务端当前协议返回 HTTP 422、 unsupported_protocol_version。这些检查在设备会话创建或 Touch 之前完成。 能力名称是闭集,只允许小写 ASCII 标识;响应列表最多 32 项,当前固定为:
text
call.signaling
conversation.direct
conversation.hide_all
conversation.mark_all_read
conversation.peer_read
conversation.preferences
custom.signals
device.sessions
media.object_authorization
message.burn_after_read
message.history
message.mutations
message.private_views
presence
push.devices
session.capability_negotiation
social.friend_remarks
social.friendships
social.governance
social.groups
social.request_deletion
typing
user.phone_resolution
user.profiles
web.browser_transport管理端 签发测试 Token、Development Easy Login 以及正式 Credential Provider 都应 提供稳定 device_id 和平台(ios、android、macos、windows、 harmonyos、web 或 linux)。每个 Token 对应一个服务端 session;同一设备 重新签发始终撤销旧 Token。single 只保留该用户最新 session, single_per_platform 每个平台只保留最新 session,multi 允许多端并存; 超过容量时按创建时间确定性淘汰最旧 session。
客户端用 /v1/sessions:list 展示设备列表,用稳定 mutation_id 调用 /v1/sessions:revoke 撤销自己的任一 session。撤销和策略淘汰会向目标 WSS 发送 SESSION_REVOKED 后关闭连接,同时清除该 session 的 Presence/Typing 租约。每个普通 HTTP 请求和 WSS 周期 Ping 都重新检查服务端 session 状态, 因此仅持有尚未过期的 JWT 不能绕过撤销。列表包含历史已撤销 session,便于用户 识别登录记录和原因;响应最多 100 条并优先返回全部活跃 session,再返回最近 历史记录。不得把服务端 session 表当作客户端密钥存储。
Presence、Typing 与 Custom Signal 只走 WSS 临时帧,不写消息历史,也不进入 /v1/sync:
/v1/presence:publish支持ONLINE、AWAY、OFFLINE。默认租约 90 秒, 允许 30–120 秒;同一用户多个 session 聚合时ONLINE优先于AWAY,所有 租约断开、撤销或过期后才投影为OFFLINE。WSS 心跳只延长当前 session 已有的 ONLINE/AWAY 状态,不会把业务刚设置的 AWAY 强行改回 ONLINE; 已过期或不存在的租约才重新建立为 ONLINE。/v1/typing:publish要求用户仍是会话成员,默认租约 5 秒,允许 1–10 秒;is_typing=false可主动结束,未结束也会由 TTL 自动投影为 false。/v1/custom-signals:publish要求发送者仍是会话成员,payload 为 1..65536 bytes,TTL 为 1–10 秒;服务端只向同会话成员的在线设备投递CUSTOM_SIGNAL,不产生 Outbox、消息或 Sync 事件。需要离线送达、审计或 历史的业务必须使用版本化持久自定义消息。- Presence 只投递给本人账号、好友和共同会话成员,并排除任一方向已拉黑的 对端;Typing 只投递给会话成员,单聊存在任一方向黑名单时拒绝发布。两个接口 另外按 App + 账号 + 设备执行每秒 10 次、突发 20 次的独立限流。
- 每个临时帧带稳定
event_id、作用域内递增sequence和expires_at_ms。客户端先按event_id去重,再只接受更高 sequence;本地 计时到期可先显示离线/停止输入,之后以服务端更高 sequence 为准。
PostgreSQL 使用共享的 UNLOGGED lease 表,使同一主库上的多副本能看到相同临时 状态,同时明确接受数据库重启/故障转移后全部离线的语义;逻辑 sequence 单独 持久化。每个活跃 WSS 都参与秒级过期触发,但单实例有全局节流,跨实例通过 LISTEN/NOTIFY 传播。临时通知是 best effort,客户端必须依赖 TTL,而不能把它 当作耐久业务事实。
/v1/social/groups:govern 使用 expected_revision 做乐观并发控制。群主可 设置管理员、转让群主、修改入群审批策略,以及原子更新群名、头像 URL、公告和 简介;群主或管理员可按权限禁言成员。成功变更会在同一事务中推进群 revision、 写审计并向成员账号流追加 GroupChanged,客户端通过 /v1/sync 更新 Schema v9 本地群投影。公告正文不会写进审计 Details。
好友删除、主动退群和群解散是独立的耐久社交生命周期操作:
/v1/social/friendships:delete把双方 Friendship 投影设为active=false,原子清除双方各自的私有备注,并分别向双方账号流追加以本人 视角编码、空备注 revision 0 的FriendshipUpsert;它不会创建黑名单关系。 需要屏蔽用户必须单独调用/v1/social/blocks:set。/v1/social/groups:leave要求调用者是当前非群主成员,并使用expected_revision做 CAS。群主必须先转让群主,或直接解散群。/v1/social/groups:dismiss只允许当前群主调用;同一事务删除全部当前成员、 推进 revision、设置member_count=0,并向解散前的全部成员账号流发送同一个GroupChanged。空成员群是终态,不能重新申请加入。
群创建、成员邀请、成员移除和主动退群还会写入 xhim.system.event@1 耐久消息,文案、actor 和 targets 均由 Server 生成; 普通 /v1/messages:send 请求该 content type 会返回 Forbidden。事件使用社交 mutation 派生的确定性 client_message_id,重试不会产生重复消息。退群事件在 成员关系移除后通过受信提交路径写入,退出者不能借此继续发送普通群消息。
三个请求都要求客户端生成独立、稳定的 mutation_id。同请求重试返回 idempotent_replay=true;复用 mutation ID 改变好友、群、操作类型或预期 revision 返回冲突。状态、账号事件、幂等回执和 friendship.delete/group.leave/group.dismiss 审计均原子提交。退出或解散 提交后,消息发送、媒体下载授权、Typing 和其他成员权限立即通过现有成员校验 失效;历史数据不会被物理删除。
好友备注是服务端权威的用户私有投影,不要求也不允许客户端再维护“本地备注优先” 规则。/v1/social/friendships:setRemark 只修改调用者 user -> peer 这一条边; 好友双方的备注、revision 和更新时间相互独立,任何一方都不能读取或覆盖另一方 的备注。请求仅允许当前活跃好友,remark 必须是合法 UTF-8、最多 512 字节, 空串表示清除;首次修改使用 expected_revision=0,之后必须精确匹配响应中的 remark_revision。每次成功设置或清除都会把 revision 加一。
备注请求要求独立、稳定的 mutation_id。相同参数重试返回 idempotent_replay=true,复用 mutation ID 改变 peer、备注或 expected_revision 返回冲突。备注状态、幂等回执、不包含备注正文的 friendship.remark.set 审计,以及仅面向调用者账号流的 FriendshipUpsert 在同一个事务提交;另一方不会收到备注事件或 Sync Hint。 好友请求首次接受或删除后重新接受时,双方备注都初始化为空、revision 为 0, 并开始新的 friendship created_at 生命周期。删除好友会原子清除双方方向的 备注,因此旧备注不会在重新加好友后恢复;inactive 好友不能设置备注。
/v1/social/friendships:update 是统一 property revision 上的原子批量 写入。每次接受 1...100 个原始不重复 peer,每个目标都必须提供 exact expected_revision,并为整批复用一个 mutation_id。请求 至少出现 remark、is-pinned 或 application-extension-json 之一,可一次 修改多项;已出现的空 remark/extension 表示显式清除。extension 只允许最多 16 KiB、嵌套深度小于 64 的严格 JSON object,并以 canonical JSON 持久化。
数据库以 peer 稳定排序锁定现有好友和属性行,先完成全部 active-friend/权限/CAS 预校验,再在同一事务写入。任一失败导致整批 回滚;每个目标无论改几个字段,revision 都只加一次。幂等回执 保存按输入顺序排列的完整权威结果;重放精确返回该结果。同步 事件只进入 owner account,每目标审计只记录 changed field 名、 字节数或布尔值,不记录 remark/JSON 内容。
通话协议位于 protocol/proto/xhim_call_v1.proto。服务端事件只保存 needs_credentials 标记,不把用户 RTC Token 广播或持久化到公共信令;接受方 在 Accept 响应取得自己的 Token,邀请方拉取 Accepted 信令时按身份即时取得 自己的 Token。
开发模式的媒体授权指向同一服务的 PUT/PATCH/GET /v1/media/objects/{opaque-key};本地对象端口支持固定大小分片 和按已提交 Offset 续传。Root 必须是专用真实目录;Root 被替换,或对象/ .partial 是 symlink、目录、设备等非普通文件时,上传、Verify、授权和下载均 fail closed。同一对象严格串行,不同对象使用固定 64 分片锁;完成文件通过同目录 hard-link 的 no-replace 语义发布,不会用可覆盖的 Rename 替换已有不同对象。
该本地 Adapter 只用于开发或可信私有环境。它使用 Go 标准库的 Lstat/Stat/SameFile 前后校验和排他创建降低路径替换风险,但标准库没有在 所有支持平台上提供完全一致的原子 no-follow 路径 API,不能作为对抗同机恶意 用户的多租户生产对象存储边界。生产必须使用私有 S3/S3-compatible Bucket、 最小权限 IAM 和短期预签名 URL。生产 S3 模式下媒体字节不经过 IM API 进程。 当前单对象上限为 256 MiB,适用于图片、语音、常见短视频和中小文件;超出上限 的大视频不受理。需要更大对象或高并发生产媒体面时,仍须交付真实 S3 multipart Session/Part 持久化、CDN 和对应容量证据。
Prepare 必须携带 conversation_id。服务端准备上传时校验上传者的会话成员 身份,并在每次下载授权时重新校验下载者仍是该会话成员,不能仅凭知道 media_id 越权下载。
头像使用独立的 profile_avatar purpose,不携带 conversation_id。头像只 接受 JPEG、PNG、HEIC 或 WebP,最大 5 MiB,仍需完成对象校验和内容审核后才 能更新当前用户资料。稳定头像 URL 只对 ready 的头像生成短期对象下载重定向; 普通消息附件即使知道 media_id 也不能从头像路由公开读取。
健康检查:
text
GET /health/live
GET /health/ready指标端点需要同一个 X-XHIM-Admin-Key,避免默认公开内部容量信息:
text
GET /metricsProduction 用户凭证由客户业务服务端换取,五端 SDK 的 Business Authentication/Credential Provider 会隐藏首次获取和续期细节。Admin Key、 签名私钥和 Cursor Key 绝不能进入客户端包、移动端配置或日志。