扩展文件存储
TLDR: store the file id, render with a signed URL.
When an extension uploads a file, Chastify returns a stable id and a short-lived signedUrl. Store the id in your extension config, state, or own database.
When your extension page loads later, call bridge.request("files.get", { fileId }) or GET /api/extensions/sessions/:sessionId/files/:fileId with the stored id. Chastify verifies that the file belongs to the same extension app, session, and lock, then returns a fresh signed R2 URL.
Render the image with <img src={file.signedUrl} />. Do not store signed URLs long-term because they expire.
扩展文件存储允许已启用的扩展上传属于锁定扩展会话的图像文件。
当扩展状态 JSON 不足以满足需求时,可以使用它,例如:
- 拼图图片
- 生成的预览
- 挑战照片
- 需要使用锁定/会话清理的扩展特定媒体。
此存储空间与 state.* 分开。请使用 state.* 存储小型 JSON 数据。文件存储仅用于存储二进制介质。
与状态端点的关系
扩展状态适用于小型会话范围的 JSON。在 iframe 中,请使用桥接读取命令,而不是直接调用 REST:
state.get
该桥接命令路由至:
GET /api/extensions/sessions/:sessionId/state
直接调用后端来写入状态使用已安装扩展程序的 API 身份验证模型:
Authorization: Bearer YOUR_APP_SCOPED_DEVELOPER_KEY
x-chastify-main-token: MAIN_TOKEN_FROM_IFRAME_HASH
请勿将开发者 API 密钥发送到 iframe/浏览器代码中。
使用后端写入的状态来存储持久引用和 UI 数据,例如:
{
"puzzleImageFileId": "file_record_id",
"selectedImageIds": ["file_record_id"],
"lastOpenedTab": "images"
}
请勿在状态中存储二进制数据、base64 编码图像、浏览器 blob: URL 或 signedUrl 的长期副本。签名 URL 会过期,应在图像渲染时使用 files.get 刷新。状态写入的大小受扩展应用程序的 stateMaxBytes 设置限制,默认值为 64 KiB。
存储模型
Chastify 将扩展文件存储在 Chastify 管理的 R2 存储中。
每个上传的文件都会被跟踪:
scope: "extension"appIdsessionId和lockId用于运行时/会话文件templateId表示已保存锁定模板所声明的文件staged、draftId和expiresAt用于尚未被认领的设置上传。extensionKeypurpose
管理门禁和配额
Admin controlled
Extension file storage is disabled by default.
Admins can enable it and configure:
- maximum bytes per file
- maximum total bytes per extension app
- maximum total bytes per active extension session
- maximum staged bytes per user and extension app
- allowed MIME prefixes
The first implementation is image-focused and should use image/* uploads. SVG is not accepted by the storage service.
If storage is disabled or R2 is not configured, upload requests fail before the server reads the multipart file body.
Staged setup uploads count toward the current user's per-app staged quota until they are claimed. Claimed files count toward the per-extension app quota. Runtime files with a session id also count toward the per-session quota.
设置暂存端点
仅当设置 UI 在扩展会话存在之前运行时才使用设置暂存。
这是一个临时上传流程。它用于设置/配置界面,用户可以在此上传文件,然后在创建锁定/会话之前关闭模态框或禁用扩展程序。暂存的文件只有在保存引用该文件的扩展程序配置时才会被永久保存。
设置界面可以在渲染上传控件之前检查可用性和当前阶段的使用情况:
GET /api/extensions/apps/:appId/files/capabilities
响应中包含 stagedQuota,代表当前用户在该扩展程序应用中未过期的暂存文件:
{
"enabled": true,
"provider": "r2",
"r2Configured": true,
"supportsStagedSetupFiles": true,
"stagedQuota": {
"bytesUsed": 123456,
"fileCount": 1,
"maxBytes": 10485760,
"remainingBytes": 10362304
}
}
POST /api/extensions/apps/:appId/files/stage
Content-Type: multipart/form-data
表单字段:
file:所需图像文件purpose:可选的简短标识符,例如jigsaw-config-imagedraftId:可选的设置草稿 ID;当一个设置模态框打开时,可重复使用相同的值。
响应包含一个稳定的 file.id 和一个用于立即预览的短期签名 URL。
示例回复:
{
"file": {
"id": "file_record_id",
"signedUrl": "https://chastify.<account-id>.r2.cloudflarestorage.com/extensions/abc.webp?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Signature=...",
"publicUrl": "https://chastify.<account-id>.r2.cloudflarestorage.com/extensions/abc.webp?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Signature=...",
"urlExpiresAt": "2026-05-28T12:10:00.000Z",
"sizeBytes": 123456,
"originalName": "puzzle.jpg",
"mimeType": "image/webp",
"purpose": "jigsaw-config-image",
"uploadedAt": "2026-05-28T12:00:00.000Z"
},
"stagedQuota": {
"bytesUsed": 123456,
"fileCount": 1,
"maxBytes": 10485760,
"remainingBytes": 10362304
}
}
页面刷新后,要恢复已上传的设置文件,请列出当前用户和扩展应用程序的暂存文件:
GET /api/extensions/apps/:appId/files/staged?purpose=jigsaw-config-image
可选查询参数:
purpose:仅返回一个安装用例的暂存文件draftId:仅返回已知安装草稿中的文件
如果省略 draftId,则 Chastify 返回当前用户该应用未过期的暂存文件。这对于设置界面来说是有意为之:用户可以上传文件、重新加载页面或切换设备,设置界面可以在锁定/模板保存之前恢复这些临时上传的文件。
暂存列表响应还包含 stagedQuota,用于报告当前用户该应用的未过期暂存使用情况:
{
"items": [],
"stagedQuota": {
"bytesUsed": 123456,
"fileCount": 1,
"maxBytes": 10485760,
"remainingBytes": 10362304
}
}
安装界面还可以刷新或删除一个暂存文件:
GET /api/extensions/apps/:appId/files/:fileId
DELETE /api/extensions/apps/:appId/files/staged/:fileId
稍后认领暂存文件
没有单独的浏览器端“声明”端点。当 Chastify 保存引用暂存文件的锁或模板扩展配置时,暂存文件会自动被声明。
这些路由会在锁定/模板文档获得 ID 后声明暂存文件,然后使用声明的文件引用再次保存扩展配置。如果声明失败,则会回滚新创建的锁定/模板,以避免暂存文件与损坏的配置关联。
共享模板说明:已接受的共享锁会在克隆的活动锁上保留源模板 ID。因此,只要任何克隆的锁仍然引用该模板,模板声明的文件就会被保留。如果源模板被删除,Chastify 会延迟删除其扩展文件,直到引用已删除模板的最后一个克隆锁被删除或归档。
要使暂存文件可被认领,请在设置界面提交的扩展配置中存储类似这样的引用:
{
"storageDraftId": "draft_123",
"images": [
{
"id": "file_record_id",
"provider": "chastify_storage",
"fileId": "file_record_id",
"url": "extension-file:file_record_id",
"title": "Puzzle image"
}
]
}
保存时,Chastify 会扫描配置中是否存在具有有效 fileId 的 provider: "chastify_storage" 记录,然后:
- 验证文件是否属于当前用户和扩展应用程序
- 拒绝已过期的暂存文件
- 拒绝已被其他锁/模板上下文声明的文件
- 标记引用了暂存文件,并声称
- 将已声明的文件绑定到已保存的锁或模板
- 在配置持久化之前,移除临时字段
signedUrl、publicUrl和urlExpiresAt。 - 从同一个
storageDraftId中删除未引用的暂存文件
未保存的废弃暂存文件会在过期后被清理程序删除。
要刷新当前用户和扩展程序所拥有的文件 ID 的设置预览:
GET /api/extensions/apps/:appId/files/:fileId
会话端点
这些端点需要正常的扩展会话授权和范围。
基本路径:
/api/extensions/sessions/:sessionId/files
能力
渲染上传控件前请检查此项。
GET /api/extensions/sessions/:sessionId/files/capabilities
需要 locks:read。
示例回复:
{
"enabled": true,
"provider": "r2",
"r2Configured": true,
"settings": {
"enabled": true,
"maxFileBytes": 10485760,
"maxBytesPerApp": 524288000,
"maxBytesPerSession": 52428800,
"maxStagedBytesPerUserPerApp": 10485760,
"allowedMimePrefixes": ["image/"]
}
}
列表文件
GET /api/extensions/sessions/:sessionId/files
需要 locks:read。
示例回复:
{
"items": [
{
"id": "file_record_id",
"signedUrl": "https://chastify.<account-id>.r2.cloudflarestorage.com/extensions/abc.webp?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Signature=...",
"publicUrl": "https://chastify.<account-id>.r2.cloudflarestorage.com/extensions/abc.webp?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Signature=...",
"urlExpiresAt": "2026-05-28T12:10:00.000Z",
"sizeBytes": 123456,
"originalName": "puzzle.jpg",
"mimeType": "image/webp",
"purpose": "puzzle-image",
"uploadedAt": "2026-05-28T12:00:00.000Z"
}
]
}
获取一个文件
当您的扩展程序已经存储了文件 ID,而只需要一个新的已签名 R2 链接时,请使用此功能。
GET /api/extensions/sessions/:sessionId/files/:fileId
需要 locks:read。
文件必须属于同一个扩展应用程序、会话和锁定。
示例回复:
{
"file": {
"id": "file_record_id",
"signedUrl": "https://chastify.<account-id>.r2.cloudflarestorage.com/extensions/abc.webp?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Signature=...",
"publicUrl": "https://chastify.<account-id>.r2.cloudflarestorage.com/extensions/abc.webp?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Signature=...",
"urlExpiresAt": "2026-05-28T12:10:00.000Z",
"sizeBytes": 123456,
"originalName": "puzzle.jpg",
"mimeType": "image/webp",
"purpose": "puzzle-image",
"uploadedAt": "2026-05-28T12:00:00.000Z"
}
}
上传文件
POST /api/extensions/sessions/:sessionId/files
Content-Type: multipart/form-data
需要 locks:write。
表单字段:
file:所需图像文件purpose:可选的简短标识符,例如puzzle-image或preview
例子:
curl "https://chastify.net/api/extensions/sessions/SESSION_ID/files" \
-X POST \
-F "purpose=puzzle-image" \
-F "[email protected]"
示例回复:
{
"file": {
"id": "file_record_id",
"signedUrl": "https://chastify.<account-id>.r2.cloudflarestorage.com/extensions/abc.webp?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Signature=...",
"publicUrl": "https://chastify.<account-id>.r2.cloudflarestorage.com/extensions/abc.webp?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Signature=...",
"urlExpiresAt": "2026-05-28T12:10:00.000Z",
"sizeBytes": 123456,
"originalName": "puzzle.jpg",
"mimeType": "image/webp",
"purpose": "puzzle-image",
"uploadedAt": "2026-05-28T12:00:00.000Z"
}
}
常见上传错误:
extension_file_storage_disabledextension_file_storage_requires_r2file_too_largeinvalid_file_typeextension_app_storage_quota_exceededextension_session_storage_quota_exceededextension_staged_user_app_storage_quota_exceeded
删除文件
DELETE /api/extensions/sessions/:sessionId/files/:fileId
需要 locks:write。
文件必须属于同一个扩展应用程序、会话和锁定。
Iframe 桥接操作
iframe 扩展程序可以使用父 Web 桥接器进行运行时文件读取。运行时文件上传和删除属于会话变更,应由后端使用应用范围的开发者 API 密钥加上 x-chastify-main-token 来执行。
const capabilities = await bridge.request("files.capabilities", {});
const refreshed = await bridge.request("files.get", {
fileId: "file_record_id"
});
image.src = refreshed.file.signedUrl;
支持的桥接动作:
files.capabilities-> 检查文件存储是否已启用以及适用的配额。files.list-> 列出当前扩展会话拥有的文件files.get-> 从存储的文件 ID 刷新一个已签名的 R2 URL
在运行时会话建立之前,设置 iframe 可以使用额外的桥接名称。在设置模式下,files.upload 用于创建暂存文件,files.list/files.staged.list 用于列出当前用户和应用程序的未过期暂存文件,而 files.delete 用于删除暂存文件。设置初始化上下文包含 storageDraftId;如果您希望 Chastify 在保存时立即从同一草稿中清除未引用的文件,请将该值作为 storageDraftId 包含在您保存的配置中。
设置 files.upload 也接受 dataUrl,适用于简单的 iframe 客户端:
await bridge.request("files.upload", {
dataUrl: canvas.toDataURL("image/webp", 0.9),
filename: "preview.webp",
purpose: "preview"
}, 60000);
尽可能优先使用 File 或 Blob 上传。仅当生成的图像较小时才使用 dataUrl,因为 base64 编码的 payload 占用内存较大。
签名链接
稳定文件标识符为 id。
使用 signedUrl 显示或下载文件。签名链接是由 Chastify 生成的短期 R2 GetObject URL。publicUrl 保留为兼容性别名,目前包含相同的签名 URL。
当签名链接过期时,调用 GET /api/extensions/sessions/:sessionId/files/:fileId 获取一个已存储文件的新链接,或调用 GET /api/extensions/sessions/:sessionId/files 刷新所有会话文件链接。
清理生命周期
扩展文件通过 BullMQ 支持的清理队列进行清理。
清理工作安排如下:
- 扩展程序应用被删除
- 在移除锁定/清理归档文件期间,锁定/会话扩展文档将被删除。
- 通过会话文件端点显式删除文件
队列机制将开销较大的 R2 删除和数据库清理操作排除在请求路径之外。如果队列入队失败,服务器会回退到响应后的进程内清理(如果存在请求上下文),或者在更低级别的生命周期清理中直接尽力清理。
演出说明
- 上传界面显示之前应该进行功能检查。
- 上传操作受到单个文件、每个会话和每个应用程序的限制。
- 配额检查使用索引的
UserFile元数据,用于scope + appId、scope + sessionId和scope + lockId。 - 清理与文件记录匹配的流,而不是将所有 URL 加载到内存中。
- 上传的栅格图像将被处理并存储为优化的网络图像。
安全说明
- 不要将扩展程序提供的图像 URL 视为可信的完成证明。
- 仅将 Chastify 颁发的文件记录存储为受信任的扩展文件。
- 将文件记录绑定到
appId、sessionId和lockId。 - 对上传和删除操作强制使用
locks:write。 - 验证 MIME 类型并拒绝 SVG。
- 在允许公众广泛使用之前,请保持管理员控制的配额功能。
- 对于任何依赖于上传文件的工作流程,都应使用服务器端验证。
直达路线与桥路
当您的扩展程序在 Chastify 中运行时,请使用 iframe 桥接。这样可以将 Chastify 的身份验证保留在父页面中,并避免将路由详细信息暴露给 iframe。
仅使用来自第一方 Chastify UI 或已具有有效扩展会话授权的受信任后端流程的直接会话路由。