Almacenamiento de archivos de extensión
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.
El almacenamiento de archivos de extensión permite que una extensión habilitada cargue archivos de imagen que pertenecen a una sesión de extensión de bloqueo.
Úselo cuando el JSON del estado de la extensión no sea suficiente, por ejemplo:
- imágenes de rompecabezas
- previsualizaciones generadas
- fotos del reto
- medios específicos de la extensión que deben limpiarse con el bloqueo/sesión
Este almacenamiento es independiente de state.*. Utilice state.* para datos JSON pequeños. Utilice el almacenamiento de archivos únicamente para medios binarios.
Relación con los puntos finales de estado
El estado de extensión es para JSON pequeños con ámbito de sesión. Desde un iframe, utilice el comando de lectura bridge en lugar de llamar directamente a REST:
state.get
Esa ruta de comando del puente apunta a:
GET /api/extensions/sessions/:sessionId/state
Las llamadas directas al backend para escribir el estado utilizan el modelo de autenticación de la API de extensión instalada:
Authorization: Bearer YOUR_APP_SCOPED_DEVELOPER_KEY
x-chastify-main-token: MAIN_TOKEN_FROM_IFRAME_HASH
No envíe claves de API para desarrolladores al código del iframe/navegador.
Utilice el estado escrito en el backend para referencias duraderas y datos de la interfaz de usuario, por ejemplo:
{
"puzzleImageFileId": "file_record_id",
"selectedImageIds": ["file_record_id"],
"lastOpenedTab": "images"
}
No almacene datos binarios, imágenes base64, URL del navegador (blob:) ni copias de larga duración de signedUrl en el estado. Las URL firmadas caducan y deben actualizarse con files.get cuando se renderiza la imagen. El tamaño de las escrituras de estado está limitado por la configuración stateMaxBytes de la aplicación de extensión, con un valor predeterminado de 64 KiB.
Modelo de almacenamiento
Chastify almacena archivos de extensión en el almacenamiento R2 administrado por Chastify.
Cada archivo subido se rastrea con:
scope: "extension"appIdsessionIdylockIdpara archivos de tiempo de ejecución/sesióntemplateIdpara archivos reclamados por una plantilla de bloqueo guardadastaged,draftIdyexpiresAtpara cargas de configuración que aún no han sido reclamadasextensionKeypurpose
Puerta de administración y cuotas
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.
Configurar puntos finales de prueba
Utilice la configuración preliminar únicamente cuando se ejecute una interfaz de usuario de configuración antes de que exista una sesión de extensión.
Este es un flujo de carga temporal. Se utiliza en pantallas de configuración donde el usuario puede cargar un archivo, cerrar la ventana modal o deshabilitar la extensión antes de crear un bloqueo o sesión. Un archivo almacenado temporalmente no es permanente hasta que se reclama guardando la configuración de la extensión que lo referencia.
Las interfaces de configuración pueden comprobar la disponibilidad y el uso actual previsto antes de renderizar los controles de carga:
GET /api/extensions/apps/:appId/files/capabilities
La respuesta incluye stagedQuota para los archivos temporales no caducados del usuario actual en esa aplicación de extensión:
{
"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
Campos del formulario:
file: archivo de imagen requeridopurpose: identificador corto opcional comojigsaw-config-imagedraftId: ID de borrador de configuración opcional; reutilice el mismo valor mientras haya una ventana modal de configuración abierta.
La respuesta contiene un código file.id estable y una URL firmada de corta duración para una vista previa inmediata.
Ejemplo de respuesta:
{
"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
}
}
Para restaurar las cargas de configuración después de actualizar la página, enumere los archivos preparados para el usuario actual y la aplicación de extensión:
GET /api/extensions/apps/:appId/files/staged?purpose=jigsaw-config-image
Parámetros de consulta opcionales:
purpose: devolver solo archivos preparados para un caso de uso de configuración.draftId: devolver solo archivos de un borrador de configuración conocido
Si se omite draftId, Chastify devuelve los archivos temporales no caducados del usuario actual para esa aplicación. Esto es intencional para las interfaces de configuración: un usuario puede cargar archivos, recargar la página o cambiar de dispositivo, y la pantalla de configuración puede restaurar esas cargas temporales antes de que se guarde el bloqueo/plantilla.
La respuesta de la lista de etapas también incluye stagedQuota, que informa sobre el uso de etapas no caducado del usuario actual para esa aplicación:
{
"items": [],
"stagedQuota": {
"bytesUsed": 123456,
"fileCount": 1,
"maxBytes": 10485760,
"remainingBytes": 10362304
}
}
Las interfaces de configuración también pueden actualizar o eliminar un archivo provisional:
GET /api/extensions/apps/:appId/files/:fileId
DELETE /api/extensions/apps/:appId/files/staged/:fileId
Reclamar archivos preparados posteriormente
No existe un punto final de "reclamación" independiente del navegador. Un archivo preparado se reclama automáticamente cuando Chastify guarda una configuración de bloqueo o extensión de plantilla que hace referencia a dicho archivo.
Estas rutas reclaman los archivos preparados una vez que el documento de bloqueo/plantilla tiene un ID, y luego guardan la configuración de la extensión con las referencias de archivo reclamadas. Si la reclamación falla, el bloqueo/plantilla recién creado se revierte para evitar que los archivos preparados queden vinculados a una configuración defectuosa.
Nota sobre plantillas compartidas: los bloqueos compartidos aceptados conservan el ID de la plantilla de origen en el bloqueo activo clonado. Por lo tanto, los archivos vinculados a la plantilla se conservan mientras cualquier bloqueo clonado siga haciendo referencia a ella. Si se elimina la plantilla de origen, Chastify retrasa la eliminación de sus archivos de extensión hasta que se elimine o archive el último bloqueo clonado que haga referencia a la plantilla eliminada.
Para que un archivo en preparación sea reclamable, almacene una referencia como esta en la configuración de la extensión que se envía a través de la interfaz de usuario de configuración:
{
"storageDraftId": "draft_123",
"images": [
{
"id": "file_record_id",
"provider": "chastify_storage",
"fileId": "file_record_id",
"url": "extension-file:file_record_id",
"title": "Puzzle image"
}
]
}
Al guardar, Chastify escanea la configuración en busca de registros provider: "chastify_storage" con un fileId válido y, a continuación:
- verifica que el archivo pertenezca al usuario actual y a la aplicación de extensión.
- rechaza archivos caducados en la etapa de preparación
- rechaza archivos que ya están siendo reclamados por otro contexto de bloqueo/plantilla
- marcas referenciadas archivos preparados según se afirma
- vincula los archivos reclamados al bloqueo o plantilla guardados.
- Elimina los campos temporales
signedUrl,publicUrlyurlExpiresAtantes de que se guarde la configuración. - elimina los archivos preparados sin referencia del mismo
storageDraftId
Los archivos temporales abandonados que nunca se guardan se eliminan mediante la limpieza una vez que expiran.
Para actualizar la vista previa de configuración de un ID de archivo propiedad del usuario actual y la aplicación de extensión:
GET /api/extensions/apps/:appId/files/:fileId
Puntos finales de sesión
Estos puntos finales requieren la autorización y los ámbitos de sesión de extensión normales.
Ruta base:
/api/extensions/sessions/:sessionId/files
Capacidades
Verifique esto antes de renderizar los controles de carga.
GET /api/extensions/sessions/:sessionId/files/capabilities
Requiere locks:read.
Ejemplo de respuesta:
{
"enabled": true,
"provider": "r2",
"r2Configured": true,
"settings": {
"enabled": true,
"maxFileBytes": 10485760,
"maxBytesPerApp": 524288000,
"maxBytesPerSession": 52428800,
"maxStagedBytesPerUserPerApp": 10485760,
"allowedMimePrefixes": ["image/"]
}
}
Listar archivos
GET /api/extensions/sessions/:sessionId/files
Requiere locks:read.
Ejemplo de respuesta:
{
"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"
}
]
}
Obtén un archivo
Utilice esta opción cuando su extensión ya tenga un ID de archivo almacenado y solo necesite un nuevo enlace R2 firmado.
GET /api/extensions/sessions/:sessionId/files/:fileId
Requiere locks:read.
El archivo debe pertenecer a la misma extensión de aplicación, sesión y bloqueo.
Ejemplo de respuesta:
{
"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"
}
}
Subir archivo
POST /api/extensions/sessions/:sessionId/files
Content-Type: multipart/form-data
Requiere locks:write.
Campos del formulario:
file: archivo de imagen requeridopurpose: identificador corto opcional comopuzzle-imageopreview
Ejemplo:
curl "https://chastify.net/api/extensions/sessions/SESSION_ID/files" \
-X POST \
-F "purpose=puzzle-image" \
-F "[email protected]"
Ejemplo de respuesta:
{
"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"
}
}
Errores comunes al subir archivos:
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
Eliminar archivo
DELETE /api/extensions/sessions/:sessionId/files/:fileId
Requiere locks:write.
El archivo debe pertenecer a la misma extensión de aplicación, sesión y bloqueo.
Acciones de puente de iframe
Las extensiones de iframe pueden usar el puente web principal para leer archivos en tiempo de ejecución. La carga y eliminación de archivos en tiempo de ejecución son mutaciones de sesión y deben ser realizadas por su backend con una clave API de desarrollador con ámbito de aplicación más 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;
Acciones de puente compatibles:
files.capabilities-> compruebe si el almacenamiento de archivos está habilitado y qué cuotas se aplican.files.list-> listar archivos propiedad de la sesión de extensión actualfiles.get-> actualiza una URL R2 firmada desde un ID de archivo almacenado
Los iframes de configuración pueden usar nombres de puente adicionales antes de que exista una sesión de tiempo de ejecución. En el modo de configuración, files.upload crea archivos preparados, files.list/files.staged.list enumera los archivos preparados no caducados para el usuario y la aplicación actuales, y files.delete elimina un archivo preparado. El contexto de inicialización de la configuración incluye storageDraftId; incluya ese valor en su configuración guardada como storageDraftId si desea que Chastify limpie los archivos sin referencia del mismo borrador inmediatamente después de guardar.
La configuración files.upload también acepta dataUrl para clientes iframe simples:
await bridge.request("files.upload", {
dataUrl: canvas.toDataURL("image/webp", 0.9),
filename: "preview.webp",
purpose: "preview"
}, 60000);
Siempre que sea posible, dé preferencia a las cargas con File o Blob. Use dataUrl solo para imágenes generadas pequeñas, ya que las cargas útiles base64 ocupan más memoria.
Enlaces firmados
El identificador de archivo estable es id.
Utilice signedUrl para visualizar o descargar el archivo. Los enlaces firmados son URL R2 GetObject de corta duración generadas por Chastify. publicUrl se mantiene como un alias de compatibilidad y actualmente contiene la misma URL firmada.
Cuando un enlace firmado caduca, llame a GET /api/extensions/sessions/:sessionId/files/:fileId para recibir un nuevo enlace para un archivo almacenado, o a GET /api/extensions/sessions/:sessionId/files para actualizar todos los enlaces de archivos de la sesión.
Ciclo de vida de la limpieza
Los archivos de extensión se eliminan mediante una cola de limpieza respaldada por BullMQ.
La limpieza está programada para cuando:
- Se elimina una aplicación de extensión
- Los documentos de bloqueo/extensión de sesión se eliminan durante la eliminación del bloqueo/limpieza del archivo.
- Un archivo se elimina explícitamente a través del punto final del archivo de sesión.
La cola evita que la costosa eliminación de R2 y la limpieza de la base de datos entren en la ruta de la solicitud. Si falla la inserción en la cola, el servidor recurre a la limpieza en proceso después de la respuesta donde existe un contexto de solicitud, o a la limpieza directa con el mejor esfuerzo posible en la limpieza del ciclo de vida de nivel inferior.
Notas de interpretación
- Las comprobaciones de capacidad deben realizarse antes de que se muestre la interfaz de usuario de carga.
- Las cargas están sujetas a límites por archivo, por sesión y por aplicación.
- Las comprobaciones de cuotas utilizan metadatos indexados
UserFileparascope + appId,scope + sessionIdyscope + lockId. - Limpia los flujos de datos que coinciden con los registros de archivos en lugar de cargar todas las URL en la memoria.
- Las imágenes rasterizadas subidas se procesan y almacenan como imágenes web optimizadas.
Notas de seguridad
- No considere las URL de imágenes proporcionadas por la extensión como prueba fehaciente de que la solicitud se ha completado correctamente.
- Almacene únicamente los registros de archivos emitidos por Chastify como archivos de extensión de confianza.
- Vincular los registros de archivos a
appId,sessionIdylockId. - Aplicar el código
locks:writepara cargas y eliminaciones. - Validar el tipo MIME y rechazar el SVG.
- Mantenga habilitadas las cuotas controladas por el administrador antes de permitir un uso público generalizado.
- Utilice la validación del lado del servidor para cualquier flujo de trabajo que dependa de archivos cargados.
Rutas directas frente a rutas puente
Utilice el puente iframe cuando su extensión se ejecute dentro de Chastify. Esto mantiene la autenticación de Chastify en la página principal y evita exponer los detalles de la ruta al iframe.
Utilice rutas de sesión directas únicamente desde la interfaz de usuario Chastify de primera parte o flujos de backend de confianza que ya tengan una autorización de sesión de extensión válida.