OneTimeSecret v0.26.19 REST API 实战指南

OneTimeSecret v0.26.19 REST API 实战:v1/v2/v3 三代接口与账户 API 全景、Basic/Token/会话 CSRF 三种鉴权实测、一次性秘密创建/两步揭示/焚毁全链路回显、防枚举统一 404 设计、脚本化集成十大坑位清单。

OneTimeSecretOneTimeSecret阅后即焚密码安全Redis缓存2026/10/26

OneTimeSecret v0.26.19 REST API 实战指南

OneTimeSecret v0.26.19 的 API 是「三代并存」的格局:v1(legacy,表单编码)、v2(stable,JSON)、v3(guest 匿名面),外加一组不带版本前缀的账户接口(/api/account/*)。三套面的鉴权规则、载荷格式、错误形态都不一样,混用是脚本集成翻车的主因。本文在运行中的 v0.26.19 实例上把三类接口全链路实测一遍——创建、读取、二次读取、口令保护、焚毁、揭示、状态查询、Token 签发——所有回显逐字照抄实测输出,并沉淀成一份坑位清单。

一、环境信息

项目值
OneTimeSecret 版本v0.26.19(/api/v2/version 实测返回 ["0","26","19"])
实例地址http://192.168.146.131:8132(部署过程见本专栏 B1/B2 篇)
测试账号admin@ots.local / q1w2e3r4(colonel 角色)
API Token账户页签发的 52 位小写字母数字串(下文以 <apitoken> 占位)
数据存储Valkey(Redis 协议兼容)

产品与配置背景见《OneTimeSecret v0.26.19 介绍与选型指南》与《OneTimeSecret v0.26.19 配置参数详解》。

二、API 全景:三代并存

接口面前缀载荷格式鉴权定位
v1(legacy)/api/v1/*表单编码(key=value)匿名可用;带 Basic 则归属到账号老集成、curl 友好,官方保留兼容
v2(stable)/api/v2/*JSON,且创建类载荷必须嵌套在 "secret" 键下Basic(用户名=邮箱,密码=API Token)现代主推面
v3(guest)/api/v3/guest/*JSON(嵌套同 v2)匿名,开关控制在配置受控匿名面
账户 API/api/account/*表单/JSON仅会话(Basic/Token 均被拒)Token 签发、改密码等账户自管理

三个探测端点可以当健康检查用(均匿名可访问,实测回显):

$ curl -s http://192.168.146.131:8132/api/v1/status
{"status":"nominal","locale":"en"} [HTTP:200]

$ curl -s -u "admin@ots.local:<apitoken>" http://192.168.146.131:8132/api/v2/status
{"success":true,"status":"nominal","locale":"en"} [HTTP:200]

$ curl -s -u "admin@ots.local:<apitoken>" http://192.168.146.131:8132/api/v2/version
{"success":true,"version":["0","26","19"],"locale":"en"} [HTTP:200]

三、鉴权模型实测

3.1 Basic:现代接口的正确姿势

v2 面用 HTTP Basic,用户名是注册邮箱、密码是 API Token(不是登录密码):

$ curl -s -u "admin@ots.local:<apitoken>" http://192.168.146.131:8132/api/v2/status
{"success":true,"status":"nominal","locale":"en"}

v1 面同样接受 Basic:匿名分享响应里 "custid":"anon",带同一组 Basic 后创建的同型请求返回 "custid":"admin@ots.local"——归属账号变了,秘密会挂到账户名下(回执与配额 accordingly)。

3.2 Bearer:直接拒绝

拿 Token 当 Bearer 用(很多现代 API 的习惯)会被明确打回,实测回显:

$ curl -s -w ' [HTTP:%{http_code}]' -X POST http://192.168.146.131:8132/api/v2/secret/conceal \
    -H 'Authorization: Bearer <apitoken>' -H 'Content-Type: application/json' \
    -d '{"secret":{"secret":"x","ttl":60}}'
{"error":"Authentication Required","message":"[AUTH_TYPE_INVALID] Invalid authorization type","timestamp":1793007805,"code":"api_key_invalid","code_scope":"credential"} [HTTP:401]

错误体里 code_scope: "credential" 说明是凭据形态不被接受(不是凭据内容错误)。

3.3 会话与 CSRF:账户 API 的唯一通道

/api/account/* 只认浏览器会话。用 Basic 调它,实测被拒:

$ curl -s -w '\n[HTTP:%{http_code}]' -X POST http://192.168.146.131:8132/api/account/apitoken -u "admin@ots.local:<apitoken>"
{"error":"Authentication Required","message":"[SESSION_NOT_AUTHENTICATED] Not authenticated","timestamp":1793007869,"code":"not_authenticated","code_scope":"customer_session"} [HTTP:401]

注意 code_scope 从 credential 换成了 customer_session——两套 401 的语义可以靠这个字段区分。正确的 Token 签发流程是浏览器会话流:登录后服务端在响应头下发 x-csrf-token(登录成功会轮换新值),后续账户 API 请求带上 X-CSRF-Token 请求头与会话 Cookie。实测按此流程完成过签发与注销:POST /api/account/apitoken 返回 {"record":{"apitoken":"…","active":true}} 形态的 JSON,POST /api/account/apitoken/destroy 注销指定 Token;POST /api/account/change-password 同属这一会话面。脚本化运维如果需要批量签 Token,现实的做法是用浏览器登录一次拿会话与 CSRF 头,或者固定使用首个 Token 不轻易轮换。

顺带排除一个误区:v2 面下没有账户接口——POST /api/v2/account/apitoken 实测返回:

{"error":"Not Found","error_type":"NotFound"} [HTTP:404]

四、标识符体系:两把钥匙别混用

一次分享会产生两个 64 位标识符(8 位短前缀 + 全长):

  • secret_key / secret_identifier:秘密本体的取件钥匙,给「读秘密」的端点用;
  • metadata_key / identifier(receipt):回执标识,给「看状态/焚毁」的端点用。

v1 的创建响应同时返回两把(见第四节回显);v2 的创建响应把它们分装在 record.receipt.identifier 与 record.secret.identifier 下。混用的实测代价:把 secret 标识符塞给回执端点,得到 404 且错误信息里回显了你传的标识符:

$ curl -s -w ' [HTTP:%{http_code}]' -u "admin@ots.local:<apitoken>" \
    http://192.168.146.131:8132/api/v2/receipt/<secret_identifier>
{"error":"identifier: 0iz7shgc5f6r2e37669x9awxdzruyzee6x7e2od1oz0tp7c8g9rl6pkg29cjih","error_type":"RecordNotFound","request_id":"2uetupmidt7t7"} [HTTP:404]

换成正确的 receipt 标识符后同一端点正常返回(见 5.5 节)。

五、v1 实战:一次性秘密全生命周期

5.1 创建与两次读取

$ curl -s -X POST http://192.168.146.131:8132/api/v1/share \
    -d 'secret=D5-api-echo-2026&ttl=3600'
{"custid":"anon","metadata_key":"fwl0pumap9sr8bda6globmhoewkouq4klqxqjunc0prtbu72uzcytumqdyvweb","secret_key":"eihc8wk9ul20g8as6hmr3j0dzcqh0j2srv1985n947nl1k3x0vejn820sfv0x8","ttl":3600,"metadata_ttl":7200,"secret_ttl":3600,"metadata_url":"http://192.168.146.131:8132/receipt/fwl0pumap9sr8bda6globmhoewkouq4klqxqjunc0prtbu72uzcytumqdyvweb","state":"new","updated":1793007740,"created":1793007740,"recipient":[],"share_domain":"","passphrase_required":false,"identifier":"fwl0pumap9sr8bda6globmhoewkouq4klqxqjunc0prtbu72uzcytumqdyvweb","secret_identifier":"eihc8wk9ul20g8as6hmr3j0dzcqh0j2srv1985n947nl1k3x0vejn820sfv0x8","has_passphrase":false,"recipients":[],"receipt_ttl":7200,"receipt_url":"http://192.168.146.131:8132/receipt/fwl0pumap9sr8bda6globmhoewkouq4klqxqjunc0prtbu72uzcytumqdyvweb"}

第一次读取(返回明文并立即销毁):

$ curl -s -w ' [HTTP:%{http_code}]' -X POST http://192.168.146.131:8132/api/v1/secret/eihc8wk9ul20g8as6hmr3j0dzcqh0j2srv1985n947nl1k3x0vejn820sfv0x8
{"value":"D5-api-echo-2026","secret_key":"eihc8wk9ul20g8as6hmr3j0dzcqh0j2srv1985n947nl1k3x0vejn820sfv0x8","share_domain":"http://192.168.146.131:8132"} [HTTP:200]

第二次读取(一次性语义生效):

$ curl -s -w ' [HTTP:%{http_code}]' -X POST http://192.168.146.131:8132/api/v1/secret/eihc8wk9ul20g8as6hmr3j0dzcqh0j2srv1985n947nl1k3x0vejn820sfv0x8
{"secret_key":"eihc8wk9ul20g8as6hmr3j0dzcqh0j2srv1985n947nl1k3x0vejn820sfv0x8","message":"Unknown secret"} [HTTP:404]

留意匿名创建的 TTL 细节:请求 ttl=3600,响应 secret_ttl: 3600 而 metadata_ttl: 7200——回执比秘密多活一倍,创建者来得及事后查状态;登录用户的 v2 创建里两者相等(见 5.1)。匿名 TTL 上限(请求 30 天被钳到 7 天)的实测与参数解释见《配置参数详解》第三节。

5.2 口令保护与防枚举设计

创建时加 passphrase=link-2026,三种读取姿势的实测回显:

# 不带口令
{"secret_key":"0zxuk13j9dv6d2vuq2bnmp44ytb1u0pgn4ivp5arr2ugzif6o0354giaj8a49i","message":"Unknown secret"} [HTTP:404]
# 口令错误
{"secret_key":"0zxuk13j9dv6d2vuq2bnmp44ytb1u0pgn4ivp5arr2ugzif6o0354giaj8a49i","message":"Unknown secret"} [HTTP:404]
# 口令正确
{"value":"D5-pass-echo","secret_key":"0zxuk13j9dv6d2vuq2bnmp44ytb1u0pgn4ivp5arr2ugzif6o0354giaj8a49i","share_domain":"http://192.168.146.131:8132"} [HTTP:200]

三个失败态(口令缺失/口令错误/秘密不存在)全部返回同一个 Unknown secret 404——接口层不区分「不存在」和「口令不对」,杜绝用错误响应差异枚举口令或探测秘密是否存在。脚本重试逻辑不要试图从 404 文案里区分场景,没得分。

5.3 焚毁:对未读秘密有效

焚毁端点挂在回执标识下:POST /api/v1/receipt/<metadata_key>/burn。对尚未被读取的秘密实测:

$ curl -s -w ' [HTTP:%{http_code}]' -X POST http://192.168.146.131:8132/api/v1/receipt/2hd2x1gxe3348zedoqt80uovj2f8ejhuixqdahyxc64m2h1jdu50ayzsp9v0xd/burn
{"state":{"custid":"anon","metadata_key":"2hd2x1gxe3348zedoqt80uovj2f8ejhuixqdahyxc64m2h1jdu50ayzsp9v0xd","secret_key":"","ttl":3600,"metadata_ttl":7200,"secret_ttl":null,"metadata_url":"http://192.168.146.131:8132/receipt/2hd2x1gxe3348zedoqt80uovj2f8ejhuixqdahyxc64m2h1jdu50ayzsp9v0xd","state":"burned","updated":1793007791,"created":1793007791,"recipient":[],"share_domain":"","identifier":"2hd2x1gxe3348zedoqt80uovj2f8ejhuixqdahyxc64m2h1jdu50ayzsp9v0xd","secret_identifier":"","recipients":[],"receipt_ttl":7200,"receipt_url":"http://192.168.146.131:8132/receipt/2hd2x1gxe3348zedoqt80uovj2f8ejhuixqdahyxc64m2h1jdu50ayzsp9v0xd"},"secret_shortkey":"gb46fznv"} [HTTP:200]

响应里 state: "burned"、secret_key 与 secret_identifier 被清空成空串、只留 8 位 secret_shortkey 作对账凭据。焚毁后再读,同样得到 Unknown secret 404。

两个边界:先读后焚(读已销毁)时同一端点返回 {"secret_key":"<你传的标识符>","message":"Unknown secret"} [HTTP:404]——焚毁动作本身也是一次性的;对已过期秘密焚毁同理。所以「焚毁兜底」脚本要在读到 200 之前就准备好 metadata_key。

六、v2 实战:现代接口

6.1 载荷嵌套规则与 conceal

v2 创建类请求的 JSON 必须嵌套在 "secret" 键下,扁平写法直接被格式校验打回:

$ curl -s -X POST http://192.168.146.131:8132/api/v2/secret/conceal -u "admin@ots.local:<apitoken>" \
    -H 'Content-Type: application/json' -d '{"secret":"C1-ttl-clamp-verify-2026","ttl":2592000}'
{"error":"Incorrect payload format","request_id":"3r93fjy5ypct0"}

正确写法(登录用户请求 30 天 TTL,实测原样生效——匿名上限不约束认证用户):

$ curl -s -X POST http://192.168.146.131:8132/api/v2/secret/conceal \
    -u "admin@ots.local:<apitoken>" -H 'Content-Type: application/json' \
    -d '{"secret":{"secret":"D5-reveal-me-2026","ttl":3600}}'
{"success":true,"record":{"receipt":{...,"identifier":"63qqltyvc04ovbeno6ocnbfoomryr4g8es9o5cf12hy9fyizbldg1evowf6m65",...,"state":"new","secret_shortid":"0iz7shgc","secret_identifier":"0iz7shgc5f6r2e37669x9awxdzruyzee6x7e2od1oz0tp7c8g9rl6pkg29cjih","secret_ttl":3600,"metadata_ttl":3600,"receipt_ttl":3600,"lifespan":3600,...,"is_burned":false,"is_expired":false,"is_orphaned":false,"is_destroyed":false,"has_passphrase":false,"kind":"conceal","source":"standard"},"metadata":{...同构...},"secret":{"identifier":"0iz7shgc5f6r2e37669x9awxdzruyzee6x7e2od1oz0tp7c8g9rl6pkg29cjih","key":"0iz7shgc5f6r2e37669x9awxdzruyzee6x7e2od1oz0tp7c8g9rl6pkg29cjih","shortid":"0iz7shgc","state":"new","lifespan":3600,"secret_ttl":3600,"has_passphrase":false,"verification":false,"display_type":"text",...}},"details":{"kind":"conceal","recipient":[],"recipient_safe":[]}}

(响应较长,上为节选,字段名与数值逐字保留。)响应是三联结构:record.receipt(回执)、record.metadata(与 receipt 同构的元数据)、record.secret(秘密本体),外加 details.kind 标注动作类型。

6.2 generate:服务端生成密码

$ curl -s -w ' [HTTP:%{http_code}]' -X POST http://192.168.146.131:8132/api/v2/secret/generate \
    -u "admin@ots.local:<apitoken>" -H 'Content-Type: application/json' -d '{"secret":{"ttl":3600}}'
{"success":true,"record":{...,"secret":{"identifier":"8hvf1ylxlqdyvejj3rm8dlc0ww3ma7k8o3fej3j9c0l57jvgoywydnyy9pm6pq",...}},"details":{"kind":"generate","recipient":[],"recipient_safe":[]}} [HTTP:200]

结构同 conceal,details.kind 变为 "generate";生成的密码本体同样要用揭示流程取(回执页只展示 60 秒,由配置 generated_value_display_ttl 控制)。生成器的长度与字符集参数见配置篇 3.4。

6.3 reveal:两步确认才吐明文

这是 v2 与 v1 最大的行为差异——揭示是两步流。第一步不带确认参数,只回确认页元数据(实测两种写法都停在第一步):

$ curl -s -w ' [HTTP:%{http_code}]' -X POST http://192.168.146.131:8132/api/v2/secret/0iz7shgc.../reveal -u "admin@ots.local:<apitoken>"
{"record":{...,"state":"new",...},"details":{"continue":false,"is_owner":true,"show_secret":false,"display_lines":4,"one_liner":null}} [HTTP:200]

# 带 JSON 体但嵌套在 "secret" 键下——同样停在第一步
{"record":{...,"state":"new",...},"details":{"continue":false,"is_owner":true,"show_secret":false,"display_lines":4,"one_liner":null}} [HTTP:200]

第二步:continue 必须是扁平键(实测嵌套写法无效),响应带出明文并销毁:

$ curl -s -w ' [HTTP:%{http_code}]' -X POST http://192.168.146.131:8132/api/v2/secret/0iz7shgc5f6r2e37669x9awxdzruyzee6x7e2od1oz0tp7c8g9rl6pkg29cjih/reveal \
    -u "admin@ots.local:<apitoken>" -H 'Content-Type: application/json' -d '{"continue":true}'
{"record":{"identifier":"0iz7shgc5f6r2e37669x9awxdzruyzee6x7e2od1oz0tp7c8g9rl6pkg29cjih","key":"0iz7shgc5f6r2e37669x9awxdzruyzee6x7e2od1oz0tp7c8g9rl6pkg29cjih","shortid":"0iz7shgc","state":"revealed","lifespan":3600,"secret_ttl":3600,"has_passphrase":false,"verification":false,"display_type":"text","created":1793007800.58156,"updated":1793007800.58156,"is_previewed":false,"is_revealed":true,"is_viewed":false,"is_received":true,"secret_value":"D5-reveal-me-2026"},"details":{"continue":true,"is_owner":true,"show_secret":true,"display_lines":4,"one_liner":true}} [HTTP:200]

state 从 new 翻转为 revealed,is_received: true,明文在 secret_value。此后再调同一端点,记录已销毁:

{"error_type":"RecordNotFound","request_id":"2fm2th4fucguh"} [HTTP:404]

口令保护的秘密在第一步后需要带 passphrase 再确认(错误口令同样拿不到明文),两步机制对防误触和防爬虫预取都有效——写自动化脚本时别把第一次 200 当成拿到密码,判断 details.show_secret 是否为 true。

6.4 status:不泄露存在性的状态查询

活跃秘密(200,含剩余秒数):

$ curl -s -w ' [HTTP:%{http_code}]' -u "admin@ots.local:<apitoken>" \
    http://192.168.146.131:8132/api/v2/secret/0iz7shgc.../status
{"record":{"identifier":"0iz7shgc...","key":"0iz7shgc...","shortid":"0iz7shgc","state":"new","lifespan":3600,"secret_ttl":3600,"has_passphrase":false,"verification":false,"display_type":"text","created":1793007800.58156,"updated":1793007800.58156,"is_previewed":false,"is_revealed":false,"is_viewed":false,"is_received":false},"details":{"current_expiration":3600}} [HTTP:200]

已销毁与从未存在的标识符返回完全相同的响应——状态端点同样不泄露存在性:

$ curl -s -u "admin@ots.local:<apitoken>" http://192.168.146.131:8132/api/v2/secret/<已销毁标识符>/status
{"record":{"state":"unknown"}} [HTTP:200]

$ curl -s -u "admin@ots.local:<apitoken>" http://192.168.146.131:8132/api/v2/secret/aaaa.../status
{"record":{"state":"unknown"}} [HTTP:200]

注意与 reveal 的差异:reveal 打在已销毁记录上返回 404 RecordNotFound,status 永远 200 + state: unknown。监控脚本想区分「秘密还活着/已消亡」,用 record.state 字段而不是 HTTP 状态码。

6.5 receipt:回执全量

用 receipt 标识符查(第三节混用示例的正确姿势):

$ curl -s -w ' [HTTP:%{http_code}]' -u "admin@ots.local:<apitoken>" \
    http://192.168.146.131:8132/api/v2/receipt/63qqltyvc04ovbeno6ocnbfoomryr4g8es9o5cf12hy9fyizbldg1evowf6m65
{"record":{...,"state":"revealed","secret_shortid":"0iz7shgc","secret_ttl":3600,"metadata_ttl":3600,"receipt_ttl":3600,"lifespan":3600,...,"is_viewed":true,"is_received":true,"is_revealed":true,"is_burned":false,"is_expired":false,"is_orphaned":false,"is_destroyed":true,"has_passphrase":false,"kind":"conceal","source":"standard","secret_state":null,"natural_expiration":"60 minutes","expiration":1793011400,"expiration_in_seconds":3600,"share_path":null,"burn_path":"receipt/63qqltyv.../burn","receipt_path":"receipt/63qqltyv...","metadata_path":"receipt/63qqltyv...","share_url":null,"receipt_url":"http://192.168.146.131:8132/receipt/63qqltyv...","burn_url":"http://192.168.146.131:8132/receipt/63qqltyv.../burn"},"details":{"type":"record","display_lines":4,"no_cache":true,"secret_realttl":null,"view_count":1,"first_access":1793007800.632271,"last_access":1793007800.632271,"has_passphrase":false,"can_decrypt":false,"secret_value":null,"show_secret":false,"show_secret_link":false,"show_receipt_link":false,"show_receipt":true,"show_recipients":false}} [HTTP:200]

(节选,字段逐字保留。)这张回执是揭示后打的:is_destroyed: true、is_revealed: true、view_count: 1,还给出 natural_expiration: "60 minutes"、焚毁路径 burn_path 与 burn_url——做「分享后跟踪」面板的数据源就是它。

6.6 语言与元信息

$ curl -s -u "admin@ots.local:<apitoken>" http://192.168.146.131:8132/api/v2/supported-locales
{"success":true,"locales":["en"],"default_locale":"en","locale":"en"}

官方分发物实际只带英文资产(配置里的 30 语言是候选集),解释见配置篇第十节。

七、v3 guest:受控匿名面

/api/v3/guest/* 是给匿名调用方保留的现代格式面,载荷嵌套规则与 v2 相同,无需任何鉴权:

$ curl -s -w ' [HTTP:%{http_code}]' -X POST http://192.168.146.131:8132/api/v3/guest/secret/conceal \
    -H 'Content-Type: application/json' -d '{"secret":{"secret":"D5-guest-echo","ttl":3600}}'
{"success":true,"record":{"receipt":{...,"owner_id":null,"state":"new","secret_identifier":"f3u2zp0n4w8j6vg3iwgr3cy93czyyenyegydka37xpg0rd376ki631i9xe9m4e","secret_ttl":3600,...},"secret":{"identifier":"f3u2zp0n4w8j6vg3iwgr3cy93czyyenyegydka37xpg0rd376ki631i9xe9m4e",...}},"details":{"kind":"conceal","recipient":[],"recipient_safe":[]}} [HTTP:200]

(节选。)与认证创建的差异:owner_id: null(无归属账号),且响应里没有 metadata 块——匿名 guest 只拿到 receipt 与 secret 两个记录。这类端点由配置 site.interface.api.guest_routes.*(环境变量 API_GUEST_ROUTES_ENABLED 及 API_GUEST_CONCEAL/GENERATE/REVEAL/BURN/SHOW/RECEIPT)逐项开关,默认全开;不需要匿名 API 的内网部署建议关掉总闸,参数见配置篇 2.3。

八、账户与 Token 管理

端点方法鉴权作用
/api/account/apitokenPOST会话 + CSRF签发新 API Token,返回 {"record":{"apitoken":"…","active":true}}
/api/account/apitoken/destroyPOST会话 + CSRF注销指定 Token
/api/account/change-passwordPOST会话 + CSRF修改登录密码

三者都在无版本前缀的 /api/account 面下(第二节实测:Basic 调用 401 SESSION_NOT_AUTHENTICATED,v2 前缀下的同名路径 404)。会话流的 CSRF 机制来自配置 site.middleware.authenticity_token(默认开启):登录响应头下发 x-csrf-token,后续写请求回填 X-CSRF-Token 头。Token 是唯一能用于 Basic 的 API 凭据,泄露后应立即 destroy 再签发——每账号多 Token 并存,按 active 字段区分。

九、脚本化集成坑位清单

十项实测沉淀,按踩中概率排序:

  1. Bearer 无效:v2 只认 Basic(邮箱 + API Token),Authorization: Bearer 得到 AUTH_TYPE_INVALID 401(第二节回显);
  2. v2 载荷必须嵌套:{"secret": "x"} 打回 Incorrect payload format,要写 {"secret": {"secret": "x", ...}};reveal 的 continue 却是扁平键——嵌套规则因端点而异,别想当然统一;
  3. 两把标识符:读秘密用 secret_key,查状态/焚毁用 metadata_key,塞错得一模一样的 404(第三节回显);
  4. v2 reveal 是两步流:第一个 200 不带明文,details.show_secret 为 false;必须再 POST {"continue":true} 才吐 secret_value;
  5. 防枚举统一 404:v1 读取与 v2 status 对「不存在/口令错/已销毁」返回同型响应,脚本别试图从错误体区分场景;
  6. 匿名 TTL 静默钳制:v1 匿名请求超长 TTL 不报错、响应里 ttl 直接变小(2592000 → 604800 实测),读回执别用请求参数而要用响应体;
  7. 匿名回执寿命是秘密的两倍:匿名 v1 创建 metadata_ttl=2×secret_ttl(7200/3600 实测),登录用户两者相等;
  8. 焚毁也是一次性:已读/已焚的秘密再调 burn 得到 Unknown secret 404,兜底脚本要容忍;
  9. 账户 API 三个「不」:不吃 Basic、不吃 Bearer、不在 /api/v2/ 前缀下——只走会话 + CSRF;
  10. 限流在暗处:匿名创建受 500 次/小时的 IP 限流(/24 网段共享桶,v0.26.11 起),触发后锁 1 小时,错误处理里给限流留退避。

十、排障交叉引用

十一、参考链接