Standard Notes 3.3.2 API 使用详解
Standard Notes 3.3.2 服务端 HTTP API 协议级实测:004 会话注册、PKCE 登录构造、同步/版本/改密/登出全链路原始回显与六个排障坑
Standard Notes 3.3.2 API 使用详解
目标:把 Standard Notes 3.3.2 版线自托管 server 的 HTTP API 从注册到登出全链路走通——004 会话体系注册、PKCE 登录的正确构造、同步/版本/改密/登出的请求体与原始回显、错误约定与限流机制,并沉淀协议级实测中踩到的坑。
本篇是协议级取证实录:所有请求与回显均为 2026-11-02 对本专栏《Standard Notes Docker Compose 部署指南》所建五服务栈(API 入口
http://127.0.0.1:3000)的原始输出,未做美化。示例账号snapi3@quzuo.net/q1w2e3r4。
1. 为什么写 API 篇而不是 CLI 篇
裁定依据(实测):server 镜像是 Node.js 微服务 monorepo,容器内 /opt/server/docker/ 只有 docker-entrypoint.sh、wait-for.sh、is-available.sh、localstack_bootstrap.sh、supervisord.conf 五个运维脚本;supervisord 拉起 9 个 node 进程(5 个 entrypoint-server.js + 3 个 entrypoint-worker.js + 1 入口),没有任何面向日常管理的 CLI(无用户管理、无备份、无状态查询命令)。管理面完全在 HTTP API 上,因此本专栏第四篇按 API 使用详解落地。
2. 鉴权体系总览:两套 token、两个 API 标识
| 体系 | 触发条件 | token 形态 | 会话能力 |
|---|---|---|---|
| legacy JWT | 注册时 version 填应用版本(如 "3.3.2") | data.token(eyJ 开头) | GET /v1/sessions 报 unsupported-account-version,改密受限 |
| session(004) | 注册时 version 填协议版本 "004" | data.session.access_token,形如 1:<sessionUuid>:<token> | 全能力:会话列表/刷新/登出/改密吊销 |
源码依据:User.supportsSessions() 即 parseInt(version) >= parseInt("004")。
api 字段决定会话载体:api=20200115 为 header-based(access_token 直接作 Authorization: Bearer);api=20240226 为 cookie-based(token 形如 2:<privateIdentifier>,原始值只在 Set-Cookie 响应头里,curl 需带 cookie jar)。
所有响应统一信封:{"meta":{"auth":{...},"server":{"filesServerUrl":...}},"data":{...}},错误在 data.error{tag,message} 或顶层 error。
3. 注册:POST /v1/users(version 大坑)
POST http://127.0.0.1:3000/v1/users
{"email": "snapi3@quzuo.net", "password": "q1w2e3r4", "api": "20200115",
"version": "004", "pw_nonce": "594cce580d24448c",
"origination": "registration", "created": 1793603036057}
HTTP 200
{"meta":{"auth":{},"server":{"filesServerUrl":"http://localhost:3125"}},
"data":{
"session":{"access_token":"1:4e102b19-83ac-4ea4-bf74-8f336a7615ea:ODdkNWI5MzA2MzJh",
"refresh_token":"1:4e102b19-...:NzMzYWEzZTRhYzNj",
"access_expiration":1798787036000,"refresh_expiration":1825159962000,
"readonly_access":false},
"key_params":{"created":1793603036057,"identifier":"snapi3@quzuo.net",
"origination":"registration","pw_nonce":"594cce580d24448c","version":"004"},
"user":{"uuid":"48383bb0-bad1-451c-ab3b-738cf33d7314",
"email":"snapi3@quzuo.net","protocolVersion":"004"}}}
要点:
version必须是协议版本号"004"。填"3.3.2"时响应变成{"user":{...},"token":"eyJ..."}(JWT 回退),后续会话体系接口全部不可用——这是实测复现率 100% 的头号坑;pw_nonce是客户端生成的随机串(参与密钥派生参数),created为毫秒时间戳;key_params原样回显,供客户端本地派生加密密钥——服务器不接触密码派生的密钥本身;- 落库核对:
users.version存004(MySQL 实测)。
4. PKCE 登录:两步走 + 一个非标准构造
第一步,把 challenge 存到服务器并取回 KDF 参数:
POST /v2/login-params
{"email": "snapi3@quzuo.net",
"code_challenge": "ODRjMDdkNGJhMDEwYzVjMzNlODgwY2QwM2EwN2RiMDgzODY2YTdmNWNkYzY1ZDlmOTJkMmQ2YzVjOWQ0ODcyZg"}
HTTP 200 → {"data":{"identifier":"...","pw_nonce":"594cce580d24448c","version":"004"}}
第二步,带 verifier 登录:
POST /v1/login
{"email": "snapi3@quzuo.net", "password": "q1w2e3r4",
"code_verifier": "ff3e0e9d3117406d990a8b961ad6a114c877fddbf6e446218e59054798719e14",
"api": "20200115"}
HTTP 200 → {"data":{"session":{...新会话...},"key_params":{...},"user":{...}}}
challenge 构造是 PKCE 标准之外的:实测正确公式是
code_challenge = base64url( sha256(code_verifier) 的十六进制字符串 )
即对 64 位 hex 摘要字符串整体做 base64url(得 86 字符),不是标准 OAuth PKCE 的 base64url(sha256(verifier) 的 32 字节原始摘要)。用标准构造提交 challenge 后登录,实测一律 401 {"message":"Invalid email or password"}(e2e_patch3 实录),换成 hex 字符串构造后 200。另外 POST /v1/login 不带 code_verifier 同样 401。
5. 会话管理三件套
列表(session token 鉴权,JWT 账号会报 unsupported-account-version):
GET /v1/sessions → HTTP 200
{"data":[
{"uuid":"4e102b19-...","api_version":"20200115","device_info":"Unknown Client on Unknown OS","current":false,...},
{"uuid":"649c5cc5-...","api_version":"20200115","device_info":"Unknown Client on Unknown OS","current":true,...}]}
刷新:POST /v1/sessions/refresh,body 必须同时带 access_token 和 refresh_token(源码 BaseSessionController.js:90),只带 refresh_token 实测 400 "Please provide all required parameters."。成功返回一对全新 token(同 sessionUuid、新 token 值):
{"access_token":"1:64f10e29-...:NTNjMzZiNWI4YTdm","refresh_token":"1:64f10e29-...:YmU5YzcwMDA0ZTQy"}
→ {"data":{"session":{"access_token":"1:64f10e29-...:YjQ1YWFmNmE3MWUw","refresh_token":"1:64f10e29-...:ZTVkYTEyOTg1NDM2",...}}}
登出:POST /v1/logout,Bearer access_token + body {"refresh_token": "..."} → HTTP 204 空响应体。登出后用该会话继续同步实测 401 {"tag":"invalid-auth","message":"Invalid login credentials."}。JWT token 调 logout 返回 401 "Invalid session token."。
6. 同步:POST /v1/items 一个端点四种用法
全部走 POST /v1/items(Bearer 鉴权),语义由请求体决定:
① 快照拉取(items 空、不带 sync_token):
{"items": [], "limit": 150}
→ {"retrieved_items":[...全量密文条目...],"saved_items":[],"unsaved":[],
"sync_token":"MjoxNzkzNjAzMDM2LjUxODgyNw=="}
② 保存新条目(客户端已加密):
{"items":[{"uuid":"79d80785-...","content_type":"Note",
"content":"{\"encrypted\": \"U2FsdGVkX1+F3NoteV1==\"}",
"created_at":1793603036524,"updated_at":1793603036524}]}
→ saved_items 回显原样密文 + 新 sync_token
③ 更新(同 uuid 提交新密文)→ 旧密文进版本历史,saved=1 conflicts=0;真冲突时条目进 unsaved。
④ 增量同步(items 空 + 上次返回的 sync_token)→ retrieved_items 只含该游标之后的变更,并返回新游标。sync_token 是 base64 的状态游标(Mjox... 解出 2:<时间戳>),客户端必须逐次替换。
content_type 是开放字符串(Note/Tag/SmartTag/Notebook 等),服务器不解析 content 内容——零知识架构的直接体现。
7. 单条检索与版本历史
GET /v1/items/:uuid → 200 {"data":{"item":{...密文条目...}}}
GET /v1/items?content_type=Note → 404(不存在该检索端点;全量检索只能走 POST 同步)
GET /v2/items/:uuid/revisions?limit=10
→ 200 {"data":{"revisions":[{"uuid":"b8389b29-...","item_uuid":"79d80785-...","content_type":"Note",...}]}}
GET /v2/items/:uuid/revisions/:rid
→ 200 {"data":{"revision":{"content":"{\"encrypted\": \"U2FsdGVkX1+F3NoteV1==\"}",...}}}
实测细节:更新一次密文后版本列表出现 1 条(旧值 V1 被归档),单版本接口取回的正是更新前的密文;revisions_revisions 表行数与 items 更新次数一致。
8. 完整性校验:POST /v1/items/check-integrity
客户端上报"我本地的条目时间戳",服务器回差异:
{"items":[{"uuid":"7005071f-...","updated_at":"2026-11-02T05:08:02.000Z"}]}
→ {"data":{"mismatches":[{"uuid":"7005071f-...","updated_at_timestamp":1793596081899662}]}}
上报时间与库内不符即进 mismatches(微秒时间戳),客户端据此触发该条目重同步。
9. 改密:PUT /v1/users/:uuid/attributes/credentials
PUT /v1/users/48383bb0-.../attributes/credentials (Bearer 当前会话)
{"current_password": "q1w2e3r4", "new_password": "q1w2e3r4-new",
"pw_nonce": "c5786435dabc40b9", "api": "20200115", "version": "004"}
→ HTTP 200,返回全新 session + key_params(pw_nonce 已换)
body 必须含 current_password;漏传实测 400 "Your current password is required to change your password. Please update your application if you do not see this option."(final2 实录)。改密成功后的连锁行为(final3 实测):旧密码登录 401,新密码 PKCE 登录 200 并返回新会话,其他既有会话被吊销(登出清理后 sessions 表仅剩 1 条)。
10. files 服务面(:3125)
GET http://127.0.0.1:3125/healthcheck → 200 "OK"
GET http://127.0.0.1:3125/v1/files → 401 {"error":{"tag":"invalid-auth","message":"Invalid valet token."}}
files 服务不走 Bearer,凭 auth 签发的 valet token(VALET_TOKEN_SECRET 签名的短期票据)授权,上传下载票据由主 API 的相关端点签发。网关每个响应的 meta.server.filesServerUrl 回显 files 地址(实测 http://localhost:3125),客户端据此拼接附件 URL。
11. 限流与排障实录
失败登录限流:auth 服务把失败计数写 Redis lock:<userUuid>,触发后登录持续被拒。测试脚本反复试错后的恢复手段(实测):
docker exec stdnotes-3125ba92-cache redis-cli --scan --pattern 'lock:*'
docker exec stdnotes-3125ba92-cache redis-cli del lock:877b6c6d-3bcb-4127-80d8-5ed51dfdd822
# 1
本篇取证过程中实际踩到并定位的坑汇总:
| # | 现象 | 根因 | 结论 |
|---|---|---|---|
| 1 | 注册返回 JWT 而非 session;GET /v1/sessions 报 unsupported-account-version | version 填了应用版本 "3.3.2" | 必须填协议版本 "004" |
| 2 | PKCE 登录恒 401 Invalid email or password(challenge 已存、密码正确) | challenge 用了标准 PKCE 的 base64url(原始摘要) | 正确构造是 base64url(sha256(verifier) 的 hex 字符串) |
| 3 | GET /v1/login-params?email=...&code_challenge=... 返回 404 Cannot GET /auth/pkce_params | 网关把 GET login-params 路由到了不存在的 auth 内部路径 | 存 challenge 用 POST /v2/login-params |
| 4 | 改密 400 | body 缺 current_password | 五个字段全传 |
| 5 | 刷新 400 "Please provide all required parameters." | 只传了 refresh_token | access_token + refresh_token 同传 |
| 6 | 连错密码后登录一直 401(密码已正确) | Redis lock:<userUuid> 限流未过期 | redis-cli del lock:* 后恢复 |
12. 落库核对(黑盒结论的白盒证据)
SELECT email,version,updated_at FROM users WHERE email='snapi3@quzuo.net';
-- snapi3@quzuo.net 004 2026-11-02 07:03:57
SELECT COUNT(*) FROM items; -- 6(全部为密文原样存储)
SELECT COUNT(*) FROM revisions_revisions; -- 6
SELECT COUNT(*) FROM sessions; -- 1(登出/改密吊销后)
13. 专栏导航
14. 跨栏相关阅读
- 《Papermerge 3.5.3 API 使用详解》——REST API 实测取证的同款写法
- 《Joplin 介绍与选型指南》——对比方案的 API/同步面
15. 参考来源
- 本平台 2026-11-02 协议级取证实录:legacy 链(JWT 注册→同步→版本→check-integrity)、004 会话链(注册→PKCE→会话→同步→版本→改密→刷新→登出→落库,17 步全通过)、新 API(20240226)与 files/web 探测链
- github.com/standardnotes/server 源码:
User.supportsSessions()、BaseSessionController.js(refresh 参数校验) - standardnotes.com/help(加密白皮书与协议版本说明)