Core
El paquete core es la base del framework. Provee la clase App, el sistema de eventos, el logger, la carga de configuracion y la jerarquia de errores.
Clase App
Sección titulada «Clase App»La App es el orquestador central. Maneja el ciclo de vida de todos los drivers y plugins.
import { App } from '@iskra-bun/core';
const app = new App({ name: 'MiApp', debug: true, logger: { level: 'debug' },});
// Registrar driversapp.register(webDriver);app.register(dbDriver);
// Instalar pluginsapp.use(miPlugin);
// Escuchar eventosapp.on('user:created', async (ctx) => { ctx.logger.info({ user: ctx.payload }, 'Usuario creado');});
// Arrancar todoawait app.start();Ciclo de Vida
Sección titulada «Ciclo de Vida»new App(config?)— Crea la instancia. Si no le pasas config, la carga automaticamente con c12.app.register(driver)— Registra un driver (no lo inicializa todavia).app.use(plugin)— Instala un plugin inmediatamente (siinstalles async,start()espera a que termine y propaga su error).app.start()— Llamainit()en todos los drivers y despuesstart()de a uno, en orden de registro. Si elinit()de un driver falla, detiene en orden inverso los drivers cuyoinit()ya corrio (incluido el que fallo; un driver sininitnunca se inicializo y no se detiene); si un driver falla al arrancar, detiene todos los drivers en orden inverso. En ambos casos cierra OpenTelemetry, relanza el error y unapp.stop()posterior ya no detiene nada.app.stop()— Llamastop()en orden inverso al de arranque (el servidor web antes que la base de datos), sigue aunque alguno falle y al final tiraLifecycleErrorcon todos los errores. Siempre cierra OpenTelemetry.
Apagado por senales
Sección titulada «Apagado por senales»Despues de start(), SIGTERM y SIGINT ejecutan app.stop() y terminan el proceso con codigo 0 (o 1 si falla o supera shutdownTimeoutMs, 10 s por defecto). Una segunda senal mientras se detiene fuerza la salida con codigo 1. Las llamadas simultaneas a app.stop() (por ejemplo un handler de senales propio junto al del App) comparten un mismo stop: cada una resuelve cuando todos los drivers se detuvieron. Configurable con shutdownSignals (lista de senales, o false para desactivarlo); bajo NODE_ENV=test viene desactivado.
Contexto (DI)
Sección titulada «Contexto (DI)»app.context es un Map cuyas claves se tipan con AppContextRegistry: los kits registran las suyas (db, kv, oracle) y vos agregas las tuyas con declaration merging. Una clave no registrada guarda unknown.
declare module '@iskra-bun/core' { interface AppContextRegistry { miServicio: MiServicio; }}
app.context.set('miServicio', instancia);const srv = app.context.get('miServicio'); // MiServicio | undefinedconst db = app.context.get('db'); // DbDriver | undefined (db-kit)Eventos
Sección titulada «Eventos»app.on(evento, handler) le da al handler ctx.payload; app.emit(evento, payload) lo envia. Los eventos declarados en AppEvents (los kits declaran los suyos: process:*, socket:connected, worker:dead-letter, …) tienen payloads tipados, y emit() los valida; el payload de cualquier otro evento es unknown.
declare module '@iskra-bun/core' { interface AppEvents { 'user:created': { id: string; email: string }; }}
app.on('user:created', (ctx) => ctx.logger.info({ email: ctx.payload.email }, 'Usuario creado'));Basado en Pino. Se crea automaticamente al instanciar la App.
app.logger.info('Mensaje informativo');app.logger.error({ err, userId: 123 }, 'Algo fallo');app.logger.debug({ data }, 'Debug info');
// Child logger (con contexto)const childLogger = app.logger.child({ module: 'pagos' });childLogger.info('Procesando pago');Niveles
Sección titulada «Niveles»trace < debug < info < warn < error < fatal
Se configura con logger.level en la config de la app.
Redaccion de secretos
Sección titulada «Redaccion de secretos»El logger censura automaticamente los campos sensibles en su salida (tanto en desarrollo como en produccion). Un campo con alguno de estos nombres, a cualquier profundidad de un objeto plano, array o error logueado, se reemplaza por [REDACTED]; los nombres se comparan sin mayusculas, - ni _ (api_key y X-API-Key son apiKey):
password, pass, passwd, apiKey, apiSecret, token, authToken, accessToken, refreshToken, idToken, secret, clientSecret, secretKey, privateKey, authorization, proxyAuthorization, cookie, setCookie, sessionId
Tambien un campo cuyo nombre termina en password, passwd, secret, token, apiKey, secretKey, privateKey o accessKey (dbPassword, x-auth-token, AWS_SECRET_ACCESS_KEY). config.env y *.data tambien se censuran, igual que los bindings de los loggers hijos (logger.child({ ... })). El objeto que se pasa no se modifica: se escribe una copia censurada.
Los errores se escriben como los escribe pino (tipo, mensaje, stack, causas y sus propios campos), con sus campos censurados de la misma forma: el config.headers.Authorization de un error de un cliente HTTP no llega al log. En los mensajes, y en el mensaje y el stack de los errores, se enmascaran la contrasena de una URL scheme://usuario:contrasena@host y los parametros de query con pinta de secreto (?authToken=, &X-Amz-Signature=). Los demas valores, y los campos de otras instancias de clases, se escriben tal cual: hay que limpiarlos antes de loguearlos.
app.logger.info({ password: 'top-secret', userId: 123 }, 'Login');// => { "password": "[REDACTED]", "userId": 123, "msg": "Login" }
app.logger.info({ config: { env: { DB_URL: '...' } } }, 'Config cargada');// => config.env aparece como "[REDACTED]"Configuracion
Sección titulada «Configuracion»La config se carga con c12, que soporta archivos .env y app.config.ts:
export default { name: 'MiApp', debug: false, logger: { level: 'info' }, db: { driver: 'sqlite', url: process.env.DATABASE_URL || 'app.db', },};AppConfig
Sección titulada «AppConfig»interface AppConfig { name: string; debug?: boolean; logger?: { level?: string }; db?: { driver: 'postgres' | 'mysql' | 'sqlite' | 'libsql'; url: string }; socket?: { enabled: boolean; port?: number; adapter?: 'bun' | 'socket.io' }; kv?: { driver: 'memory' | 'redis'; connection?: string | Record<string, unknown> }; processes?: Record<string, ProcessConfig>; [key: string]: unknown; // otras secciones: leelas con su propio tipo}RestartBackoffConfig
Sección titulada «RestartBackoffConfig»Cada entrada de processes acepta un restartBackoff opcional que controla el backoff exponencial entre reinicios. Todos sus campos son opcionales y tienen valores por defecto:
interface RestartBackoffConfig { /** Demora inicial en ms antes del primer reinicio. Default: 1000 */ initialMs?: number; /** Tope maximo de demora en ms. Default: 30000 */ maxMs?: number; /** Multiplicador aplicado a la demora tras cada reinicio. Default: 2 */ factor?: number;}Entorno
Sección titulada «Entorno»Las protecciones de produccion de Iskra rigen en todo entorno salvo NODE_ENV=development y test: sin definir, production y nombres como staging cuentan como produccion. Asi, un deploy que olvida NODE_ENV igual tiene cookies de sesion Secure, rechaza secretos de ejemplo y URLs de auth en http plano, ignora disableCSRFCheck y loguea JSON; app.start() loguea un warning cuando NODE_ENV no esta definido. Para desarrollo local defini NODE_ENV=development (lo hacen el bun dev y el .env.example de los templates).
Los mismos chequeos se exportan para tu codigo. Leen NODE_ENV al llamarse, asi que un binario compilado con bun build respeta el valor con el que corre (un process.env.NODE_ENV literal se reemplaza al compilar):
import { isDevelopmentEnv, isProductionEnv, nodeEnv } from '@iskra-bun/core';
nodeEnv(); // 'staging', o undefined si no esta definidoisProductionEnv(); // true salvo con NODE_ENV 'development' o 'test'isDevelopmentEnv(); // lo contrarioOpenTelemetry
Sección titulada «OpenTelemetry»Con una seccion otel, y los paquetes opcionales @opentelemetry/* instalados, app.start() arranca el SDK de OpenTelemetry para Node antes que los drivers, con exportadores OTLP/HTTP y las auto-instrumentaciones de Node (fs queda apagada):
import { SECRET_QUERY_PARAMS } from '@iskra-bun/core';
otel: { endpoint: 'https://otel-collector.example.com:4318', // default: http://localhost:4318 serviceName: 'orders-api', instrumentations: { '@opentelemetry/instrumentation-http': { ignoreIncomingRequestHook: (req) => req.url === '/health', redactedQueryParams: [...SECRET_QUERY_PARAMS, 'sid'], }, '@opentelemetry/instrumentation-dns': { enabled: false }, },},Cada entrada de instrumentations se le pasa tal cual a esa instrumentacion, asi que sus propias opciones (hooks, redactedQueryParams…) funcionan, no solo enabled.
Los spans HTTP exportan la URL de cada request (url.full, o http.url y http.target en versiones anteriores), y un query string suele llevar una credencial. Sus parametros con pinta de secreto se exportan con el valor REDACTED: los de SECRET_QUERY_PARAMS (token, access_token, refresh_token, api_key, key, secret, password, code, state, ticket, signature, sig, X-Amz-Signature…), comparados sin mayusculas, - ni _. redactedQueryParams (requests salientes) y redactedQueryParamsServer (entrantes) en la instrumentacion HTTP eligen la lista, o [] la apaga; un requestHook propio se sigue ejecutando, despues de la redaccion. Los tokens en un path, y los headers que elijas capturar, se exportan tal cual.
El log de arranque nombra el endpoint solo por su origen: su path, query o userinfo pueden llevar una API key. Un endpoint http:// sin cifrar en otro host (que no sea localhost, una direccion privada, o un nombre sin punto o terminado en .local o .internal) ademas registra un warning, porque los spans viajarian sin cifrar: usa https://.
Spans propios
Sección titulada «Spans propios»traced(name, fn) envuelve una función en un span nombrado por su capa, dominio y método (repo.usuarios.buscar): el span es el activo mientras corre, así los spans que se inician adentro (una consulta con instrumentOracle() de db-oracle, un request saliente) son sus hijos, y un throw o un rechazo lo marcan como error. Sin un SDK de OpenTelemetry cuesta un span no-op.
import { traced } from '@iskra-bun/core';
export const buscarUsuario = traced('repo.usuarios.buscar', async (id: number) => oracle.one('SELECT * FROM usuarios WHERE id = :id', { id }),);Errores
Sección titulada «Errores»Todos los errores extienden IskraError:
import { IskraError, ConfigError, DriverError, ErrorCodes } from '@iskra-bun/core';
// Crear un error con contextothrow new ConfigError('Falta la URL de la base de datos', { context: { field: 'db.url' },});
// Encadenar errorestry { await conectar();} catch (err) { throw new DriverError('No se pudo conectar al driver', { cause: err, context: { driver: 'postgres' }, });}
// Verificar tipoif (err instanceof IskraError) { console.log(err.code); // 'CONFIG_INVALID' console.log(err.context); // { field: 'db.url' } console.log(err.toJSON());}ErrorCodes
Sección titulada «ErrorCodes»Codigos disponibles: INTERNAL_ERROR, NOT_INITIALIZED, CONFIG_INVALID, CONFIG_MISSING, DRIVER_INIT_FAILED, DRIVER_START_FAILED, DRIVER_STOP_FAILED, LIFECYCLE_START_FAILED, LIFECYCLE_STOP_FAILED, BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, METHOD_NOT_ALLOWED, CONFLICT, PAYLOAD_TOO_LARGE, VALIDATION_ERROR, RATE_LIMITED, SERVICE_UNAVAILABLE, TIMEOUT, DATABASE_ERROR, CONNECTION_ERROR, QUERY_ERROR, MIGRATION_ERROR, QUEUE_ERROR, JOB_ERROR, y mas. web-kit responde un error con el status de su código (NOT_FOUND → 404, ver Errores HTTP).
Una app agrega sus propios códigos por declaration merging, y ErrorCode los acepta:
declare module '@iskra-bun/core' { interface ErrorCodeRegistry { ORDER_LOCKED: true; }}