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

@exvio/os-backend-core

v0.4.0

Published

Shared backend building blocks for the Wellous OS hub-and-spoke fleet.

Readme

@exvio/os-backend-core

Shared backend building blocks for the Wellous OS hub-and-spoke fleet.

This package plays the same governance role for backend infrastructure that @exvio/os-theme plays for frontend UI: one implementation, versioned releases, and deliberate upgrades in each consumer. It ships TypeScript source because every current consumer runs on Bun and already type-checks with bundler module resolution.

Scope law

The package owns infrastructure that has the same contract in multiple Wellous backends. It must not own a product's routes, role model, database schema, migrations, environment names, tenant table catalogue, or domain services.

Dependency direction is one-way:

consumer backend  --->  @exvio/os-backend-core
        adapters  --->  db / redis / logger / config supplied by the app

Core modules never import a consumer repository. Infrastructure that needs an application service must be exposed as a factory and receive a narrow adapter.

v0.1 tenant API

The first release owns the shared AsyncLocalStorage tenant context and the Kysely AST plugin that scopes tenant-aware queries.

import {
  TenantFilterPlugin,
  bypassTenant,
  tenantContext,
  withTenant,
} from '@exvio/os-backend-core/tenant'

const db = new Kysely<Database>({
  dialect,
  plugins: [
    new TenantFilterPlugin(TENANT_AWARE_TABLES, {
      context: tenantContext,
      // Transitional v0.1 policy. Move to `throw` after every non-HTTP entry
      // point has an explicit tenant or bypass scope.
      onMissingContext: 'passthrough',
    }),
    // Query profiler goes after the tenant filter so it sees executed SQL.
  ],
})

await tenantContext.run({ tenantId: requestTenantId, bypass: false }, handleRequest)
await withTenant(jobTenantId, runBackgroundJob)
await bypassTenant(runReviewedCrossTenantOperation)

The consumer remains the source of truth for TENANT_AWARE_TABLES. The plugin only receives that ReadonlySet<string>; it does not know any product schema. The constructor snapshots the set so later mutation cannot alter the boundary. Entries are case-sensitive bare table names. A tenant-scoped table migration or rename must update the consumer catalogue and its sentinel test in the same PR.

Security contract

  • HTTP tenant middleware must wrap the entire downstream request in tenantContext.run(...).
  • WebSockets, background jobs, cron, queues, workers, and fire-and-forget tasks must use withTenant(...) for each tenant; HTTP context is not a substitute for an explicit non-HTTP entry boundary. withTenant accepts only positive safe-integer tenant ids.
  • bypassTenant(...) is only for reviewed cross-tenant operations.
  • No context currently preserves the fleet's existing behaviour: the plugin passes the query through by default. Consumers must explicitly configure the transitional policy, audit every entry point, and then switch to onMissingContext: 'throw'.
  • Raw sql\...`` fragments are opaque to Kysely's AST transformer and must include their own tenant predicate.
  • Joined tenant tables are currently supported for INNER and LEFT joins (including their lateral forms). RIGHT, FULL, CROSS, USING-join, and APPLY variants fail loudly when the joined side is tenant-aware because placing a predicate in ON would not safely filter their preserved side.
  • DELETE ... USING tenant tables are scoped in the outer WHERE.
  • A tenant-scoped UPDATE cannot assign tenant_id. Multi-table targets and dynamic/raw update-column names fail loudly because the package cannot prove their ownership semantics. Ownership changes require a separately reviewed cross-tenant operation.
  • INSERT ... SELECT and INSERT ... DEFAULT VALUES against a tenant-aware table fail loudly. The only escape is a narrowly scoped bypassTenant(...) operation that explicitly guarantees the projected tenant id.
  • UPSERT and REPLACE variants fail loudly for tenant-aware targets. Until a conflict key can be proven to include tenant_id, they can otherwise match and mutate another tenant's row through a globally unique key.
  • With an active tenant context, MERGE currently fails loudly for every table because the Kysely AST cannot yet be rewritten safely.
  • Compile and execute a query inside the intended context; do not compile it in one scope and execute it in another.

All context users and TenantFilterPlugin must resolve the same installed package instance. Do not mix a copied tenant context with the packaged plugin. The optional context constructor argument makes this dependency explicit and protects linked development/HMR from using a different singleton instance.

tenantContext is exposed for compatibility with HTTP middleware. Application code must not call its enterWith() or disable() methods; prefer withTenant(), bypassTenant(), and getCurrentTenant().

Migration and seed commands must make their policy explicit. During the v0.1 transition they may rely on passthrough; before enabling strict mode, move them to a dedicated Kysely instance without the tenant plugin or wrap each reviewed operation in the appropriate scope.

v0.2 AI runtime (alpha)

The AI subpath centralizes provider transport and runtime behaviour without owning a product's prompts, tools, tier rules, RAG, workflows, config tables, or usage ledger.

import { createAIClient } from '@exvio/os-backend-core/ai'
import { createDeepSeekProvider } from '@exvio/os-backend-core/ai/providers/deepseek'
import { createGeminiProvider } from '@exvio/os-backend-core/ai/providers/gemini'
import { getCurrentTenant } from '@exvio/os-backend-core/tenant'

const ai = createAIClient({
  providers: new Map([
    ['gemini', createGeminiProvider()],
    ['deepseek', createDeepSeekProvider()],
  ]),
  // The app still owns DB/Redis config and must keep this tenant-isolated.
  configSource: {
    loadConfig: tenantId => loadAIConfig(tenantId, { fallback: false }),
  },
  tenantResolver: () => {
    const context = getCurrentTenant()
    return context && !context.bypass ? context.tenantId : undefined
  },
  missingTenantPolicy: 'throw',
  telemetry: observeRedactedInvocation,
})

const result = await ai.generateText('Rewrite this clearly.', {
  tenantId,
  lane: 'text',
  metadata: { feature: 'editor.rephrase' },
})

// A reliable product ledger must use the returned result, not telemetry.
await recordUsage({
  tenantId,
  provider: result.provider,
  model: result.model,
  usage: result.usage,
})

The application config adapter returns explicit provider/model pairs:

{
  providers: {
    gemini: { apiKey: '...' },
    deepseek: {
      apiKey: '...',
      endpoint: 'https://api.deepseek.com/chat/completions',
    },
  },
  lanes: {
    text: { provider: 'gemini', model: 'gemini-2.5-flash' },
    vision: { provider: 'gemini', model: 'gemini-2.5-flash' },
    chat: { provider: 'deepseek', model: 'deepseek-v4-flash' },
    embedding: { provider: 'gemini', model: 'gemini-embedding-001' },
  },
}

There is no model-prefix inference or silent provider fallback in the new API. parseAIConfig() also applies Core's built-in model lifecycle policy to every named lane, including custom lanes and explicit { provider, model } entries. Retired identifiers fail at config load; only an explicit forward data migration may use replacementForRetiredAIModelId() to rewrite stored config. Credentials are loaded per invocation and provider instances are not shared between tenants. Missing tenant context fails by default. The built-in tenant resolver intentionally ignores a bypassTenant(...) scope; a reviewed cross-tenant operation must pass a positive tenantId on every AI call. A positive explicit tenant must match a normal active tenant context; callers cannot use an option to escape request tenancy. Enter a reviewed bypass scope before making an intentional cross-tenant call. A custom endpoint must be HTTPS, contain no URL credentials, and pass the application's validateEndpoint policy before an Authorization header is created. A provider factory's immutable official endpoint is the only endpoint accepted without that hook. The built-in Gemini adapter additionally accepts only Google's official API origin.

AIClientOptions.fetch is an injectable transport for HTTP providers such as DeepSeek. The production Gemini adapter uses the official @google/genai SDK, which cannot honor that injected transport; it fails loudly if the client is configured with a custom fetch instead of silently bypassing the application's transport policy.

Every text, content, chat, structured-output, stream, and embedding result uses the same actual-model and token-usage envelope. A stream may retry only before its first emitted event. Tool arguments and structured JSON fail closed rather than becoming {}. Telemetry is scalar, secret-free, non-blocking, and best-effort: a sink failure never changes a successful AI result, and telemetry must not be used as a reliable billing or usage ledger. Persist non-stream usage from the returned AIResult; for streaming, persist the result carried by the terminal finish event.

Adding providers and lanes

The registry and lane names are open strings, so adding OpenAI, Anthropic, Azure AI, an on-prem model, or a future vendor does not require changing the consumer API. Implement an AIProviderFactory, declare its capabilities, add provider conformance tests, register it in each application's config adapter, and add an explicit { provider, model } selection. Product call sites remain on generateText, generateContent, generateChat, streamChat, generateObject, or embed.

Multiple providers and lanes can be active for the same tenant at the same time. For example, text may use DeepSeek, vision Gemini, chat a private provider, and embedding a dedicated embedding model. New product-specific lanes such as workflow, classification, or assessment require only a new config entry; Core does not maintain a closed lane enum. Automatic fallback, load balancing, or cost routing must be an explicit application policy and may never silently change the reported provider/model.

Model/lane selection and prices are deliberately application-owned; rates are not bundled. Use createPriceCatalog() with an operations-owned version/effective date and calculateCostUsd(). Unknown models return null, not a misleading zero-dollar value. Product prompts, personas, RAG/vector-store policy, tool execution/authorization, and workflows also remain outside core.

v0.3 Changelog runtime (alpha)

The Changelog subpath centralizes immutable release-note catalogues, weighted language negotiation, and per-subject acknowledgement state without owning an application's content, HTTP routes, user table, or migrations.

import {
  createChangelogCatalogue,
  createChangelogService,
} from '@exvio/os-backend-core/changelog'

const catalogue = createChangelogCatalogue({
  entries: appReleaseNotes, // newest first; versions are opaque keys
  locales: ['en', 'cn', 'bm'] as const,
  defaultLocale: 'en',
  languageTags: { zh: 'cn', ms: 'bm' },
  maxVersionLength: 64, // must match the consumer database column
})

const changelog = createChangelogService({
  catalogue,
  acknowledgements: appAcknowledgementStore,
})

const state = await changelog.getState({
  subjectId: authenticatedUserId,
  acceptLanguage: request.headers.get('Accept-Language'),
})

The first array entry is the latest release. Core never sorts versions by SemVer, text, or display date. Every acknowledgement must name an exact known version, so a late acknowledgement for an older screen may make a newer release reappear but can never silently hide it. The catalogue snapshots and freezes consumer input, requires the default-locale body, and allows older entries to fall back when an optional translation is absent.

The acknowledgement store always receives both tenantId and subjectId. The default resolver reads the package tenant context, ignores bypass scopes, and fails when no tenant exists. An explicit tenant is allowed for reviewed background or bypass work, but it may not disagree with an active non-bypass tenant. Consumer repositories should use the supplied subject as their scope; they must not query by user id alone.

Hono status codes, request-body limits, authentication, Kysely queries, database migrations, release-note Markdown, and frontend sanitization remain application-owned. HTTP adapters should project only the documented response fields and send personalized responses with Cache-Control: no-store, private.

v0.3 Guide runtime (alpha)

The Guide subpath owns the common file-content mechanics used by every backend: manifest validation, deterministic grouping, locale fallback, bounded Markdown reads, frontmatter parsing, visibility policies, and removal of private ai-context authoring comments.

import path from 'node:path'
import {
  createFileGuideContentSource,
  createGuideService,
} from '@exvio/os-backend-core/guide'

const guides = createGuideService({
  locales: ['en', 'cn'] as const,
  defaultLocale: 'en',
  categories: appGuideCategories,
  entries: appGuideEntries,
  source: createFileGuideContentSource({
    root: path.resolve('src/data/guides'),
    maxFileBytes: 512_000,
  }),
  // These are separate by design: hiding an item in a catalogue is not
  // automatically authorization for its detail route.
  listPolicy: (entry, context) => includeGuideInCatalogue(entry, context),
  readPolicy: (entry, context) => mayReadGuide(entry, context),
})

await guides.validate() // CI/startup asset audit
const document = await guides.load({ category, slug, locale, context })

Category, slug, and locale identifiers are validated before file access. The file source requires an explicit absolute root, resolves real paths, rejects resolved symlink escapes, and caps reads. The root must be a Git/deployment-owned read-only asset directory; this adapter is not a sandbox for a directory that an untrusted process can rename or rewrite concurrently. Manifest input is deeply snapshotted and case-insensitive filesystem collisions fail at construction. Locale fallback returns both the requested and actual resolved locale.

Frontmatter intentionally supports only title and description, handles BOM, LF, and CRLF, and rejects duplicate or unknown fields. User-facing bodies always remove <!-- ai-context ... -->; an unclosed marker removes the rest of the document rather than leaking private authoring material. The first API does not offer a raw or bulk AI loader.

Applications retain their Markdown, manifest metadata, role model, Hono/i18n adapter, MCP and AI prompt adapters, and any tenant-specific overrides. Core policies receive the app's generic context but do not know UserRole. Routes must explicitly project response fields instead of serializing the returned extensible manifest entry. Markdown-to-HTML rendering and DOM sanitization are a frontend/theme responsibility.

v0.4 Auth runtime (alpha)

The first Auth slice centralizes framework-neutral login policy, atomic OAuth transactions, non-bearer session-management handles, lifetime calculation, and secret redaction. It does not own users, roles, Hono routes, Redis, Kysely, provider credentials, or product authorization.

import {
  createLoginPolicy,
  createOAuthBrowserExchangeManager,
  createOAuthStateManager,
  createSessionBearerToken,
  createSessionManagementHandle,
} from '@exvio/os-backend-core/auth'

const policy = createLoginPolicy({
  mode: tenantIsHubManaged ? 'hub_managed' : 'standalone',
  enabled: { email: true, google: true, microsoft: false, passkey: true },
})
policy.assertAllowed({ surface: 'google', phase: 'start' })

const oneTimeStore = {
  put: (key, value, ttlSeconds) => redis.set(key, value, ttlSeconds),
  // This must be one Redis GETDEL command or one Lua script.
  take: key => redis.getdel(key),
}
const oauthState = createOAuthStateManager({ store: oneTimeStore })
const browserExchange = createOAuthBrowserExchangeManager({ store: oneTimeStore })

const state = await oauthState.issue({
  tenantId,
  provider: 'google',
  redirectUri,
  verifier: pkceVerifier,
  initiatingSessionId: browserTransactionCookie,
})

const exchangeCode = await browserExchange.issue({
  tenantId,
  browserTransaction: browserTransactionCookie,
  provider: 'google',
  userId,
  // Optional revocation epoch captured before provider authentication.
  sessionGeneration,
})

The OAuth manager generates the state token and stores only its SHA-256-derived key. Consumption binds the exact tenant, provider, redirect URI, and optional initiating session, then burns the record before returning the PKCE verifier. Missing, malformed, expired, replayed, tenant-swapped, provider-swapped, and redirect-swapped transactions fail with a secret-free AuthError.

After a successful provider callback, the browser exchange manager creates a short-lived, one-time code bound to the tenant, browser transaction, provider, user, optional session revocation generation, and expiry. It stores only a SHA-256-derived key and atomically burns the record before comparing the tenant and browser binding, so mismatch, replay, and concurrent consumption fail closed. The application owns the Secure, HttpOnly, SameSite=Lax transaction cookie, Hono exchange endpoint, final login policy/user checks, and session creation. A callback redirects with only the exchange code; a bearer token, PKCE verifier, OAuth state, and user projection must never be placed in the URL.

Passkey alpha boundary

The Auth alpha also provides framework-neutral Passkey challenge transactions, distributed rate-limit decisions, recent-auth evaluation, HMAC-derived WebAuthn user handles, and signature-counter compare-and-swap rules. Registration binds the positive tenant and user ids, initiating session, recent authentication time, RP ID, origin, and excluded credentials. Authentication binds the positive tenant id, optional user, RP ID, origin, and allowed credential set. Both flows consume stored state atomically before comparison; missing, replayed, concurrent, malformed, purpose-swapped, or context-swapped finishes fail with a secret-free PasskeyAuthError.

Core supplies no Redis or SQL client. A challenge adapter must implement atomic take with Redis GETDEL (or one equivalent Lua command), and authentication issuance also requires atomic putIfAbsent. The application supplies the Redis Lua rate-limit adapter and tenant-aware SQL counter CAS, plus Hono routes, SimpleWebAuthn verification, RP configuration, trusted-proxy address policy, credential persistence, recent-auth UX, security notifications, and durable audit storage. Durable Passkey audit is not implemented by this alpha and is a release blocker wherever policy or compliance requires it.

createSessionManagementHandle() derives a stable smh_v1_... handle for a session-management screen. That handle cannot authenticate a request. A consumer must still verify the authenticated owner before revocation and must never return or log the underlying bearer. New sessions use the disjoint sat_v1_... namespace from createSessionBearerToken(); raw random values, smh_v1_... handles, and oas_v1_... states are not session bearers. Consumers enforce both idle and absolute expiry with remainingSessionTtl() and revoke the request's previous session after every successful authentication or privilege-changing rotation.

The mode and surface matrix is shared, but role hierarchies are not. Each product's action permissions, object access, workflow ownership, entitlements and API-key/MCP scope catalogues remain application policy.

redactAuthSecrets() and redactAuthStorageKey() produce log-safe values. The storage-key redactor bundles only generic auth namespaces (session:, oauth:state:, passkey:auth:, password:reset: and similar); an application's own credential-bearing key prefixes are passed on each call, for example redactAuthStorageKey(key, ['ticket:', 'invite:']). A key that matches no prefix is returned unchanged, so an unlisted namespace is a logging leak, not a redaction.

Install

The package is on the public npm registry under the @exvio scope. No git, ssh or AWS credentials are needed on the consumer side, and a Docker build that runs bun install needs nothing beyond registry access: no Git, no SSH client, no key to forward.

bun add @exvio/os-backend-core
{ "dependencies": { "@exvio/os-backend-core": "^0.4.0" } }

Pin exactly (0.4.0, no caret) where a consumer must not pick up a minor: on a 0.x line every minor may carry a breaking change.

Published versions are immutable and every consumer upgrades explicitly. A pre-release version (0.5.0-alpha.1) is published under the next dist-tag and never moves latest. Contributor workflow, gates and the release procedure are in the source repository's CONTRIBUTING.md, which is not part of the package.