npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@customyai/storage

v0.1.1

Published

Customy Storage for connected applications: upload bytes from memory (checksum and multipart handled), read metadata, signed download URLs and trash, over @customyai/core (typed errors, retries with Retry-After, idempotency, Access machine tokens). Server

Readme

@customyai/storage

Customy Storage para el servidor de una app conectada: subir archivos que ya tienes en memoria, leer sus metadatos, obtener URLs firmadas de descarga y mandarlos a la papelera. Sobre @customyai/core: reintentos con Retry-After, idempotencia, errores tipados y la identidad de la app en Customy Access.

npm install @customyai/storage @customyai/core
import { createMachineTokens, discoverPlatform } from "@customyai/core";
import { createStorage, CustomyStorageError } from "@customyai/storage";

const platform = await discoverPlatform(process.env.CUSTOMY_ISSUER!);
const machineTokens = createMachineTokens({ issuer: platform.issuer, clientId: process.env.CUSTOMY_STORAGE_CLIENT_ID!, clientSecret: process.env.CUSTOMY_STORAGE_CLIENT_SECRET!, platform });
const storage = createStorage({ platform, machineTokens, scopes: ["storage:files:read", "storage:files:write", "storage:files:delete"] });

const file = await storage.files.upload(
  { data: bytes, fileName: "recibo.jpg", mimeType: "image/jpeg", metadata: { expenseId: "exp_1" }, folder: "recibos/2026" },
  { idempotencyKey: `receipt-${expenseId}` },
);

try {
  const { url } = await storage.files.downloadUrl(file.id, { inline: true, expiresIn: 300 });
} catch (error) {
  if (error instanceof CustomyStorageError && error.code === "FILE_SCAN_PENDING") {
    // Recién subido: el antivirus aún no terminó (423, `retryable: true`). Reintenta en unos segundos.
  }
}

const { data, mimeType, name } = await storage.files.download(file.id);
await storage.files.trash(file.id);
  • Credencial: machineTokens (+ platform) pide tokens con audiencia customy-storage y los scopes que indiques (STORAGE_SCOPES); también acepta accessToken (un token de Access ya emitido o un proveedor). Solo servidor.
  • Scopes: storage:files:read (ver y descargar), storage:files:write (subir) y storage:files:delete (papelera). Sin el scope de una operación, 403 STORAGE_SCOPE_REQUIRED.
  • Aislamiento: cada app ve solo los archivos que ella subió; los de otra app o del Workspace no existen para ella (404 STORAGE_ITEM_NOT_FOUND). Lo que sube queda privado a la app, dentro de su propia carpeta (folder crea subcarpetas).
  • Subida: files.upload acepta hasta 25 MiB (STORAGE_MAX_UPLOAD_BYTES). Calcula el SHA-256, pide las partes firmadas, las sube directo al almacén (reintenta cada parte ante red, 408, 429 y 5xx) y completa. Si Storage ya tiene esos bytes no sube nada. La misma idempotencyKey (por defecto una nueva por llamada) devuelve el mismo archivo al repetir. Si ya hay un archivo con ese nombre en la carpeta, Storage le añade un sufijo: guarda el id, no el nombre.
  • Antivirus: un archivo recién subido está scanStatus: "pending"; hasta que queda clean, downloadUrl y download fallan con 423 FILE_SCAN_PENDING (retryable: true). FILE_SCAN_FAILED y FILE_QUARANTINED no se arreglan reintentando.
  • Errores: CustomyStorageError (un CustomySdkError con service: "storage" y retryable); code es el de la API o uno del SDK (SDK_*, FILE_TOO_LARGE, UPLOAD_PART_FAILED, DOWNLOAD_FAILED).
  • checksumSha256: lo devuelve upload; get no lo conoce y da null.

Credencial de la app

Una clave M2M de Customy Access con audiencia customy-storage y los scopes que necesita la app; si es la de una app conectada, emitida como llave de integración (machineIdentityType: "integration", machineIdentityId: <id de la aplicación conectada>) para que sus archivos queden separados de los de otras apps del mismo entorno aunque la llave rote.

POST {access}/api/admin/env/{environmentId}/api-keys
{ "name": "mi-app-storage", "scopes": ["storage:files:read", "storage:files:write", "storage:files:delete"],
  "allowedAudiences": ["customy-storage"], "machineIdentityType": "integration",
  "machineIdentityId": "<applicationId>", "ownerService": "mi-app", "expiresInDays": 90 }

La respuesta trae id (el clientId) y rawKey (el clientSecret, una sola vez).