Archivos
Almacenamiento de archivos por contenido, con carga por partes para archivos grandes.
Descripción general
Archivos almacena el contenido binario de tu aplicación (imágenes, documentos, exportaciones, grabaciones) en https://data.inovacc.dev, con cargas por partes para todo lo que sea grande. Ofrece dos maneras de direccionar un archivo:
- por ruta, en la carpeta de tu aplicación:
PUT /v1/files/users/123/avatar.png, como un bucket de almacenamiento en la nube nombra los archivos, con listados por carpeta, metadatos de usuario y carpetas compartidas con otras aplicaciones de tu espacio de trabajo; - por contenido, dentro de una base de datos:
PUT /v1/databases/{db}/blobs/{sha256}, donde el propio SHA-256 del archivo es su nombre, de modo que un registro de Datos puede apuntar a él con un campoblob.
El problema que resuelve es mantener los archivos junto a tus datos sin operar almacenamiento: Inovacc elige dónde viven los bytes, los verifica contra su hash, elimina duplicados dentro de tu carpeta y los devuelve con encabezados que impiden que un archivo descargado se ejecute en un navegador.
Usa Archivos para todo lo que sean bytes en lugar de campos. Conserva la descripción estructurada de un archivo (propietario, título, estado) como un registro en Datos. Para hacer que un archivo se pueda buscar por significado, agrégalo a una colección de Conocimiento y búsqueda vectorial.
Conceptos
La carpeta de tu aplicación. Cada aplicación tiene su propia carpeta. La carpeta se elige a partir de la credencial, nunca de la solicitud: una credencial vinculada a una aplicación usa la carpeta de esa aplicación, y una credencial no vinculada a ninguna aplicación recibe 403 application_required en toda ruta de rutas de archivo. Las rutas son UTF-8, de 1 a 1,024 bytes, separadas por /, sensibles a mayúsculas y minúsculas, sin ningún segmento vacío, . ni ..; una ruta que no esté ya en esa forma se rechaza, nunca se reescribe en silencio.
Metadatos. Una escritura puede llevar un Content-Type y tus propios encabezados X-File-Meta-<name> (2 KiB en total). Ambos regresan en cada lectura. Una escritura reemplaza por completo el contenido, el tipo y los metadatos del archivo.
Hashes. Cada archivo se identifica por su SHA-256. Envía X-File-Sha256 con una escritura y los bytes se verifican contra él (400 hash_mismatch, no se almacena nada). Dentro de la carpeta de una aplicación, dos rutas con los mismos bytes comparten una sola copia almacenada; la copia se conserva hasta que se va la última ruta. La deduplicación nunca cruza aplicaciones ni organizaciones.
Blobs de bases de datos. Un blob se direcciona por el SHA-256 en su ruta dentro de una base de datos. Cargarlo de nuevo responde 200 en lugar de 201. Cualquiera con permiso de lectura sobre la base de datos que conozca el hash puede leerlo: las reglas de registros no se aplican a los blobs.
Carpetas compartidas. Una aplicación puede crear una carpeta team que aparece para toda aplicación con acceso como shared/team/.... El creador es su propietario y concede a otras aplicaciones del mismo espacio de trabajo read o read_write. Una aplicación sin acceso no puede saber que la carpeta existe: toda operación responde 404.
Archivos grandes. Hasta 100 MiB van en una sola solicitud. Por encima de eso, hasta 5 GiB, cargas por partes y luego vinculas el resultado a una ruta (o se convierte en el blob).
Cómo funciona
Toda llamada lleva Authorization: Bearer <credential>; no hay encabezado de organización, porque tu organización y tu aplicación se obtienen de la credencial. El permiso que se verifica es lectura, escritura o eliminación de blobs. Una página web puede usar estas rutas con una clave publicable desde un origen que su aplicación liste.
Escribir por ruta. PUT /v1/files/{path} con los bytes y un Content-Length (obligatorio). La respuesta es 201 para una ruta nueva o 200 para un reemplazo: {"path","size","sha256","content_type","updated_at","metadata"} y ETag: "<sha256>".
Leer. GET /v1/files/{path} devuelve el archivo completo (no se admiten rangos) con Content-Type, ETag, X-File-Sha256, X-File-Updated-At y tus encabezados X-File-Meta-*. Cada descarga también lleva X-Content-Type-Options: nosniff, una Content-Security-Policy que la aísla y Cache-Control: no-store, de modo que una página HTML cargada nunca pueda ejecutarse como tu sitio. HEAD devuelve solo los encabezados.
Listar. GET /v1/files?prefix=photos/&delimiter=/ lista los archivos directamente bajo una carpeta como items y cada subcarpeta una vez en prefixes. Sigue next_cursor hasta que sea null: una página puede ser más corta que limit y aun así tener una siguiente.
Por partes. POST /v1/files-uploads/{sha256} inicia una carga (o responde 200 con exists: true cuando tu carpeta ya contiene esos bytes). Envía las partes de la 1 a la 10,000 con PUT .../{upload_id}/parts/{n}, cada una de entre 5 MiB y 100 MiB excepto la última, y luego POST .../complete con la lista de partes y sus etag. Inovacc vuelve a leer el objeto completo y verifica su SHA-256 y su tamaño. Por último, PUT /v1/files/{path} con X-File-Sha256 y un cuerpo vacío le da una ruta. Los blobs de bases de datos usan los mismos cuatro pasos bajo /v1/databases/{db}/blobs/{sha256}/uploads.
Las cargas, descargas y eliminaciones se registran en el Registro de actividad con el hash y el tamaño del archivo.
Primeros pasos
Necesitas una clave secreta vinculada a una aplicación, con permisos de blobs (Autenticación).
- Carga un archivo.
PUT /v1/files/reports/2026-10.csvcon el archivo como cuerpo yContent-Type: text/csv(consulta los ejemplos). La respuesta es 201 con susha256. - Léelo de vuelta.
GET /v1/files/reports/2026-10.csv. Los bytes llegan conETagigual al hash del paso 1. - Lista la carpeta.
GET /v1/files?prefix=reports/&delimiter=/. Tu archivo está enitems. - Reemplázalo. Haz
PUTen la misma ruta con bytes nuevos. La respuesta es 200, con un nuevosha256. - Elimínalo.
DELETE /v1/files/reports/2026-10.csvresponde 204; un segundoGETes 404file_not_found.
Casos de uso
Fotos de perfil desde el navegador. Una aplicación web carga cada avatar en users/<id>/avatar.png con una clave publicable y guarda el sha256 devuelto en el registro del usuario, para que una página pueda saber cuándo cambió la foto.
Exportaciones compartidas con una aplicación asociada. Una aplicación de reportes crea la carpeta compartida exports, concede read a la aplicación de facturación, y escribe archivos mensuales bajo shared/exports/. La aplicación de facturación los lista y los descarga; revocar la concesión surte efecto en su siguiente solicitud.
Medios grandes con integridad. Una herramienta de video calcula el hash de una grabación de 2 GiB en el cliente, la carga en partes de 100 MiB y la completa. Inovacc la almacena solo si el SHA-256 coincide, de modo que una transferencia corrupta nunca se convierte en un archivo.
Límites y precios
| Límite | Valor |
|---|---|
| Archivo o blob en una sola solicitud | 100 MiB, Content-Length obligatorio |
| Archivo o blob cargado por partes | 5 GiB; partes de 5 MiB a 100 MiB (excepto la última), numeradas de 1 a 10,000 |
| Ruta | 1,024 bytes |
| Prefijo de listado | 256 bytes |
| Metadatos por archivo | 2 KiB |
| Página de listado | 1 a 200 (por defecto 50) |
| Nombre de carpeta compartida | 1 a 64 letras, dígitos, _ o - |
| Tasa | 600 solicitudes por minuto por titular de credencial, por ubicación |
Precios: a consultar.
Errores
| Estado | Código | Qué significa y qué hacer |
|---|---|---|
| 400 | invalid_path, invalid_prefix | La ruta o el prefijo no está en forma canónica; corrígelo de tu lado. |
| 400 | invalid_metadata | Content-Type o X-File-Meta-* incumple las reglas o supera 2 KiB. |
| 400 | hash_mismatch | Los bytes no coinciden con el SHA-256 que enviaste; no se almacenó nada. |
| 400 | invalid_part, invalid_limit, invalid_cursor, invalid_grantee | Corrige las partes de la carga, el listado o la concesión. |
| 401 | invalid_credentials | Envía una credencial válida. |
| 403 | application_required | Las rutas de archivo necesitan una credencial vinculada a una aplicación. |
| 403 | forbidden | Falta un permiso, o un beneficiario de read intentó escribir. |
| 404 | file_not_found, blob_not_found, share_not_found, upload_not_found | No existe, o no tienes acceso a él. |
| 409 | already_exists, not_empty | El nombre de la carpeta compartida ya está en uso; vacía la carpeta antes de eliminarla. |
| 411 | length_required | Envía Content-Length. |
| 413 | payload_too_large | Más de 100 MiB en una solicitud (usa partes) o más de 5 GiB. |
| 429 | rate_limited | Reduce el ritmo. |
| 503 | service_unavailable | Reintenta más tarde. |
Buenas prácticas
- Envía
X-File-Sha256en cada escritura para que una carga dañada se rechace en lugar de almacenarse. - Usa partes por encima de 100 MiB, y comienza con
POST .../uploads: respondeexists: truecuando los bytes ya están ahí. - Guarda el
sha256, no solo la ruta, en tus registros; te dice exactamente a qué contenido se refiere un registro. - Sigue
next_cursorhasta el final al listar; las páginas cortas son normales. - Concede
reada menos que un asociado deba escribir, y revoca las concesiones que ya no necesites. - No dependas de que los blobs sean privados por regla: cualquiera que pueda leer la base de datos y conozca el hash puede leer un blob.
Relacionados
- Datos: registros que apuntan a archivos con campos
blob - Registro de actividad: cargas y descargas con sus hashes
- Conocimiento y búsqueda vectorial: haz que los documentos se puedan buscar
- Autenticación, Errores, Límites
Autenticación
Toda llamada lleva tu clave; tu organización viene de ella. Consulta la guía de autenticación.
AuthorizationEndpoints
PUT /v1/databases/{database}/blobs/{sha256}
GET /v1/databases/{database}/blobs/{sha256}
DELETE /v1/databases/{database}/blobs/{sha256}
POST /v1/databases/{database}/blobs/{sha256}/uploads
Ejemplo
Lenguaje
⋮
cURL
# The body is {} here: see parameters.
curl -X PUT "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}" \
-H "Authorization: Bearer $INOVACC_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'# The body is {} here: see parameters.
curl -X PUT "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}" \
-H "Authorization: Bearer $INOVACC_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
TypeScript
// The body is {} here: see parameters.
const body: Record<string, unknown> = {};
const url = "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}";
const response = await fetch(url, {
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
console.log(response.status, await response.text());// The body is {} here: see parameters.
const body: Record<string, unknown> = {};
const url = "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}";
const response = await fetch(url, {
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
console.log(response.status, await response.text());
Python
import json
import os
import urllib.request
# The body is {} here: see parameters.
body = {}
request = urllib.request.Request(
"https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}",
data=json.dumps(body).encode(),
method="PUT",
headers={
"User-Agent": "inovacc-python-sample",
"Authorization": "Bearer " + os.environ["INOVACC_API_KEY"],
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())import json
import os
import urllib.request
# The body is {} here: see parameters.
body = {}
request = urllib.request.Request(
"https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}",
data=json.dumps(body).encode(),
method="PUT",
headers={
"User-Agent": "inovacc-python-sample",
"Authorization": "Bearer " + os.environ["INOVACC_API_KEY"],
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Go
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
const url = "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}"
// The body is {} here: see parameters.
const body = `{}`
func main() {
req, err := http.NewRequest("PUT", url, strings.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INOVACC_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(out))
}package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
const url = "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}"
// The body is {} here: see parameters.
const body = `{}`
func main() {
req, err := http.NewRequest("PUT", url, strings.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INOVACC_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(out))
}
Rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
// Cargo.toml: serde_json = "1"
fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("INOVACC_API_KEY")?;
// The body is {} here: see parameters.
let body = serde_json::json!({});
let response = reqwest::blocking::Client::new()
.put("https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}")
.bearer_auth(api_key)
.json(&body)
.send()?;
println!("{} {}", response.status(), response.text()?);
Ok(())
}// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
// Cargo.toml: serde_json = "1"
fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("INOVACC_API_KEY")?;
// The body is {} here: see parameters.
let body = serde_json::json!({});
let response = reqwest::blocking::Client::new()
.put("https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}")
.bearer_auth(api_key)
.json(&body)
.send()?;
println!("{} {}", response.status(), response.text()?);
Ok(())
}
JavaScript
// The body is {} here: see parameters.
const body = {};
const url = "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}";
const response = await fetch(url, {
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
console.log(response.status, await response.text());// The body is {} here: see parameters.
const body = {};
const url = "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}";
const response = await fetch(url, {
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
console.log(response.status, await response.text());
PHP
<?php
// The body is {} here: see parameters.
$body = <<<'JSON'
{}
JSON;
$curl = curl_init('https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}');
curl_setopt_array($curl, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INOVACC_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($curl);
echo curl_getinfo($curl, CURLINFO_HTTP_CODE), ' ', $response, "\n";
curl_close($curl);<?php
// The body is {} here: see parameters.
$body = <<<'JSON'
{}
JSON;
$curl = curl_init('https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}');
curl_setopt_array($curl, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INOVACC_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($curl);
echo curl_getinfo($curl, CURLINFO_HTTP_CODE), ' ', $response, "\n";
curl_close($curl);
Ruby
require "net/http"
require "uri"
uri = URI('https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}')
# The body is {} here: see parameters.
body = <<~'JSON'
{}
JSON
request = Net::HTTP::Put.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('INOVACC_API_KEY')}"
request["Content-Type"] = "application/json"
request.body = body
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
end
puts "#{response.code} #{response.body}"require "net/http"
require "uri"
uri = URI('https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}')
# The body is {} here: see parameters.
body = <<~'JSON'
{}
JSON
request = Net::HTTP::Put.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('INOVACC_API_KEY')}"
request["Content-Type"] = "application/json"
request.body = body
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
end
puts "#{response.code} #{response.body}"
Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Main {
public static void main(String[] args) throws Exception {
String apiKey = System.getenv("INOVACC_API_KEY");
// The body is {} here: see parameters.
String body = "{}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.method("PUT", HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Main {
public static void main(String[] args) throws Exception {
String apiKey = System.getenv("INOVACC_API_KEY");
// The body is {} here: see parameters.
String body = "{}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.method("PUT", HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
C#
using System.Text;
// The body is {} here: see parameters.
var body = "{}";
var url = "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}";
var apiKey = Environment.GetEnvironmentVariable("INOVACC_API_KEY");
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Put, url);
request.Headers.Add("Authorization", $"Bearer {apiKey}");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");using System.Text;
// The body is {} here: see parameters.
var body = "{}";
var url = "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}";
var apiKey = Environment.GetEnvironmentVariable("INOVACC_API_KEY");
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Put, url);
request.Headers.Add("Authorization", $"Bearer {apiKey}");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
Kotlin
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
fun main() {
val apiKey = System.getenv("INOVACC_API_KEY")
// The body is {} here: see parameters.
val body = "{}"
val request = HttpRequest.newBuilder()
.uri(URI.create("https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.method("PUT", HttpRequest.BodyPublishers.ofString(body))
.build()
val response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString())
println("${response.statusCode()} ${response.body()}")
}import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
fun main() {
val apiKey = System.getenv("INOVACC_API_KEY")
// The body is {} here: see parameters.
val body = "{}"
val request = HttpRequest.newBuilder()
.uri(URI.create("https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.method("PUT", HttpRequest.BodyPublishers.ofString(body))
.build()
val response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString())
println("${response.statusCode()} ${response.body()}")
}
Swift
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
// The body is {} here: see parameters.
let body = #"""
{}
"""#
let apiKey = ProcessInfo.processInfo.environment["INOVACC_API_KEY"] ?? ""
let url = URL(string: "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}")!
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = Data(body.utf8)
let (data, response) = try await URLSession.shared.data(for: request)
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
print(status, String(decoding: data, as: UTF8.self))import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
// The body is {} here: see parameters.
let body = #"""
{}
"""#
let apiKey = ProcessInfo.processInfo.environment["INOVACC_API_KEY"] ?? ""
let url = URL(string: "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}")!
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = Data(body.utf8)
let (data, response) = try await URLSession.shared.data(for: request)
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
print(status, String(decoding: data, as: UTF8.self))
Detalles de la fuente
- Id
- identity/component-taxonomy/capabilities#files
- Repositorio
- identity
- Ruta
- contracts/component-taxonomy/catalog/capabilities.json
- Commit
- 08ef3cd75d82
- Id
- identity/component-taxonomy/quickstart#files
- Repositorio
- identity
- Ruta
- contracts/component-taxonomy/catalog/capabilities.json
- Commit
- 08ef3cd75d82