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

@ontemper/resources

v0.5.0

Published

Reads Temper variables and resource records from workflow code.

Readme

@ontemper/resources

Reads variables and resource records from Temper workflow code, plus the legacy partners and connections surface. Protocol clients (SFTP, PostgreSQL, Kafka, S3, Azure Blob, HTTP) are not part of this package: resolve a record, then pass it to the client you install.

Installation

npm install @ontemper/resources

Usage

import { createServer } from 'unnbound-events';
import { env, selectPartnerFromEvent } from '@ontemper/resources';

const server = createServer();

// Determine partner from URL parameter
server.get(
  '/webhook/:partnerId',
  selectPartnerFromEvent((c) => c.request.params?.partnerId),
  (c) => ({ status: 200, body: {} }),
);

// Determine partner from authorization header
server.get(
  '/webhook',
  selectPartnerFromEvent((c) => {
    const apiKey = c.request.headers.authorization;

    if (!apiKey) return;

    return env.partners.search((partner) => partner.variables.tryGet('UNNBOUND_API_KEY') === apiKey);
  }),
  async (c) => {
    // Get current partner (set by selectPartnerFromEvent middleware)
    const partner = env.partners.current;

    // Get a specific partner by key
    const specificPartner = env.partners.get('EXTERNAL_PARTNER_ID');

    // Get a global connection
    const globalConnection = env.connections.get('TURVO_OAUTH');

    // Get a partner connection
    const partnerConnection = partner.connections.get('TURVO_OAUTH');

    // Get a global variable (live through the runtime resolver when the workflow carries a resolver token)
    const globalApiKey = await env.variables.get('GITHUB_API_KEY');

    // Get a partner variable
    const partnerApiKey = partner.variables.get('GITHUB_API_KEY');

    return { status: 200, body: {} };
  },
);

API Reference

env

The main environment accessor singleton.

env.initialize()

Loads the initial environment configuration before a workflow handles requests, then refreshes it on a background interval. It retries transient startup failures before exiting when configuration cannot be loaded.

env.reload()

Refreshes the environment configuration. Before the first successful load, reload() waits for configuration so the workflow has the state it needs. After that load, reload() returns immediately while the cached configuration is fresh; once it expires, reload() waits for the refresh and rejects if it fails. Scheduled background refreshes keep the last known good configuration and log a warning.

Configuration refreshes every five minutes by default. Set UNNBOUND_ENV_CACHE_MAX_AGE to a positive duration in milliseconds to change the interval. Set UNNBOUND_ENV_API_TIMEOUT to change the timeout, in milliseconds, for the configuration API and for every runtime resolver call (env.variables.get, env.resources.*). The timeout defaults to 5 seconds.

The SDK selects the Configuration for the current environment from TEMPER_ENVIRONMENT (sandbox, staging, or production). Temper injects it in every workflow environment; nothing to configure. If neither the Temper name nor the legacy UNNBOUND_ENVIRONMENT name is set, initialization makes no request, retries path resolution three times, logs the final error, and exits the process.

env.partners

  • env.partners.current - Get the current partner (requires selectPartnerFromEvent middleware)
  • env.partners.get(key) - Get a partner by key
  • env.partners.getAll() - Get all partners
  • env.partners.has(key) - Check if a partner exists
  • env.partners.search(predicate) - Search for a partner by predicate

env.connections

  • env.connections.get(key, type?) - Get a global connection by key
  • env.connections.has(key) - Check if a connection exists

env.variables

  • await env.variables.get(key) - Read a variable. With a runtime-resolver token (UNNBOUND_RUNTIME_TOKEN or UNNBOUND_RUNTIME_TOKEN_FILE) the value is read live from the API and cached for 30 seconds; without one it comes from the boot-time configuration snapshot. A missing variable rejects with a ServerError.
  • env.variables.has(key) - Whether the boot-time snapshot holds the variable (synchronous).

Logs mask the secret values read through the runtime resolver: the values the API flags as secret are registered as redaction masks for the current trace. When a trace ends, its masks are kept process-wide so a value returned from it stays masked in later logs. Every resolver read registers the secrets it returns, cache hits included, so a trace that reads a cached secret masks it too. Treat masking as a safeguard, not a guarantee: do not log secrets. Boot-time snapshot values (the no-token path of env.variables, and env.partners) carry no secret flag and are not masked; do not log them.

env.resources

Resources are typed records (an SFTP server, a PostgreSQL database, an HTTP API, ...) stored on the platform and read live through the runtime resolver. inspect and resolve take the resource key; select takes { resourceTypeKey, match }. Every method accepts an optional { cache?: boolean; ttlMs?: number } as its last argument. A resolver failure rejects with a ServerError whose cause names the target and the API error. When no resource answers an inspect (a key outside the workflow's reach) or a select (no match), the error is a ResourceNotFoundError: a ServerError with status 404. Map it to your route's status without walking the cause chain; left unhandled, it answers 404. A resolve miss stays a ServerError with status 500: the API answers a missing record and a broken reference inside a found record with the same 404, and a broken reference is a configuration fault. Its cause still carries the 404. A missing Variable also stays a ServerError with status 500.

import { env, HttpApiResource, ResourceNotFoundError } from '@ontemper/resources';
import { ServerError } from 'unnbound-events';
import { registerRedactionMasks } from 'unnbound-logger-sdk';

// Mask the presented key first: a failed lookup leaves the request, key included, in the request's logs.
registerRedactionMasks([tenantKey]);
const { key } = await env.resources
  .select({ resourceTypeKey: 'http_api', match: { tenantKey } })
  .catch((error: unknown) => {
    if (error instanceof ResourceNotFoundError) throw new ServerError({ status: 401, message: 'Unauthorized.' });
    throw error;
  });
const api = await env.resources.resolve(key, HttpApiResource);

An ordinary resolver read writes no log line or span to the workflow's logs. During a resolver outage, a read that serves a stale value (see below) logs one warning per key per outage and resolves; a read that has no stale value to serve rejects. The API records each uncached read in the Resources usage history.

  • await env.resources.inspect(key) - The record without its secret fields: resourceTypeKey, scope, literal values.
  • await env.resources.resolve(key, schema?) - The value with every secret field filled in. With a Zod schema, the value is parsed by it: defaults are applied, and a mismatch rejects with an error that names the field. The exported schemas' messages never include the value; a custom schema's own refinement messages are forwarded as written, so do not interpolate the value into them. The secret values are registered as redaction masks, as for every resolver read.
  • await env.resources.select({ resourceTypeKey, match }) - { key } of the record whose named selectable field matches (for example the tenantKey an http_api tenant presents at ingress). A type may mark several fields selectable; match names exactly one. Cached per type and match. A miss is cached for 10 seconds, so a burst of requests with one unknown key costs one resolver call; a record created meanwhile is found once that time passes, or at once by a cache: false select. Once 60 distinct matches got a 404 this minute, those 404s and each later one stay cached until the minute ends, unless the bounded miss cache (10,000 entries) evicts them first; a match the API has not answered is always sent (see Tuning).

The package exports one Zod schema, and its type, for each typed built-in resource: SftpResource, HttpApiResource, PostgresqlResource, KafkaResource, S3Resource, AzureBlobResource, As2IdentityResource, and As2PartnerResource. These readers take the resolved record value, not a transport envelope. The partner's identity reference arrives inline. generic and custom resource types resolve as a JsonObject; pass your own Zod schema to parse them.

All readers follow the platform's lenient-read contract:

  1. Unknown fields pass through unchanged.
  2. In HttpApiResource, unknown authentication variants parse as { type: 'unsupported', variant, fields }. Refuse that result in code that does not know the new variant. The PostgreSQL, SFTP, Kafka, S3, and Azure Blob auth discriminators are closed: an unknown variant fails the read.
  3. Closed values that choose transport security, credential placement, or AS2 cryptography reject with UnknownRecordValueError. The error identifies the record and field path, never the value.
  4. Readers apply catalog defaults, including PostgreSQL sslmode: 'verify-full', HTTP API headers and authentication, S3 region and path style, Azure endpoint suffix, OAuth token request format, API-key placement and name, and AS2 partner security settings.

Resolve a record and pass it to the protocol client:

import { env, SftpResource } from '@ontemper/resources';
import { TemperSftpClient } from '@ontemper/sftp';

const record = await env.resources.resolve('partner-sftp', SftpResource);
const sftp = new TemperSftpClient(record);
await sftp.connect();

TemperSftpClient maps revision-2 auth variants (password, private_key, and private_key_and_password) to ssh2. The two-factor variant uses ssh2's authentication handler to send the password after public-key authentication. It also maps algorithms and continues to verify hostKeyFingerprints.

PostgreSQL Resources use the dedicated @ontemper/postgresql SDK, which owns TLS, pool limits, and private builder transport:

import { createPostgresqlPool } from '@ontemper/postgresql';

const pool = await createPostgresqlPool('orders-db');

getDatabaseTunnelAuthorization(key) is the transport adapter's authorization reader. It returns { url, token } using the current runtime token provider. Call it on each physical connection; do not cache, log, or persist its result. It does not open a connection or authorize a destination locally. The API performs those access checks.

Use kafkaConfig to build a KafkaJS configuration:

import { env, kafkaConfig, KafkaResource } from '@ontemper/resources';
import { Kafka } from 'kafkajs';

const resource = await env.resources.resolve('events', KafkaResource);
const kafka = new Kafka(kafkaConfig(resource));

The mapper derives TLS from tls, sets clientId when the record has one, maps PLAIN and SCRAM SASL, caches OAuth client-credentials tokens, and supports MSK IAM through the optional aws-msk-iam-sasl-signer-js peer dependency. Install that peer only when a record uses aws_msk_iam. When awsAuth.type is assume_role, also install the optional @aws-sdk/credential-providers peer dependency. sasl_plain, oauthbearer, and aws_msk_iam records require tls. The return type, KafkaClientConfig, is assignable to KafkaJS KafkaConfig and does not import kafkajs.

S3, Azure Blob, and HTTP API records carry explicit auth variants. Map the resolved fields to the client library you use; do not restore legacy flat credential fields or infer an ambient identity from missing credentials.

The env.resources.http(), sftp(), postgresql(), kafka(), and storage() client factories and the createHttpClient connection client are removed. Use the PostgreSQL SDK for database Resources; resolve records for other installed protocol clients.

Set UNNBOUND_RUNTIME_RESOLVE_CACHE_MAX_AGE (milliseconds) to change the 30 second resolver cache. The cache holds at most 10,000 values and drops the least recently used first. Each read is one entry, so one tenant costs a select, a resolve, and its Variables: 1,000 entries would hold only about 450 tenants per pod and evict live tenants into a herd of re-fetches. A record is a few hundred bytes to a few KB, so 10,000 entries stay well under 50 MB. Misses live in their own cache and never evict values.

When the API is unreachable, times out, answers with a 5xx status, sends a response cut short, or no runtime token is available, a read serves the last good value past its TTL: up to 15 minutes for resolve, inspect, and env.variables.get, and up to 5 minutes for select. This applies to env.variables.get only in a workflow configured with a runtime token; without one, it reads the boot-time snapshot and never calls the API. The select window is shorter because a selection often authorizes a tenant: a tenant key revoked just before an outage keeps matching for at most the TTL plus 5 minutes. The legacy env.partners snapshot already keeps a revoked key for up to 5 minutes without any outage. The first stale read of a key logs one warning; a successful refresh resets it, so a later outage logs again. During the outage the SDK retries a key only when a read of it arrives at least 5 seconds after the last failed attempt, so a recovered API is seen on the first such read. Set TEMPER_RUNTIME_RESOLVE_STALE_IF_ERROR_MAX_AGE and TEMPER_RUNTIME_RESOLVE_SELECT_STALE_IF_ERROR_MAX_AGE (milliseconds) to change the windows; 0 turns stale serving off. A stale value can be an old credential: a value rotated or revoked during an outage stays in use until a retry of that key reaches the API or the window ends. A retry updates only the value the SDK returns: a client the workflow built from an earlier read keeps that credential until the workflow reads the resource again and rebuilds or updates the client.

A 401, 403, 404, or 409 answer never serves a stale value and drops the cached one, including when a cache: false read receives it. cache: false reads skip both caches and never serve a stale value. Any API answer to a cache: false read (a value or a refusal) clears what older reads cached for that key, and a refresh that started before it does not write its older answer back; a read that started after it keeps its answer, even when the bypass answers later. The bypass answer itself is not cached: the next ordinary read calls the API again, and during an outage it has no stale value to serve. Stale serving applies only to a value the SDK still holds: past 10,000 values, the least recently used is evicted, and an outage read of an evicted key rejects.

The runtime token is sent as long as it is valid, even when the token provider has not refreshed it. An expired token waits at most 2 seconds for a fresh one, then the read fails as an outage.

Tuning

Each setting applies per workflow process (one pod). An environment variable takes a non-negative integer, in milliseconds for durations; an invalid value falls back to the default.

| Setting | Default | Environment variable | Protects against | Costs | | ----------------------- | -------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Value TTL | 30 s | UNNBOUND_RUNTIME_RESOLVE_CACHE_MAX_AGE (min 1) | One resolver call per read | An edit reaches the workflow up to one TTL late | | Stale window (reads) | 15 min | TEMPER_RUNTIME_RESOLVE_STALE_IF_ERROR_MAX_AGE (0 off) | An API outage failing resolve, inspect, and env.variables.get | A credential rotated or revoked during an outage stays in use for up to the window | | Stale window (select) | 5 min | TEMPER_RUNTIME_RESOLVE_SELECT_STALE_IF_ERROR_MAX_AGE (0 off) | An API outage failing every tenant lookup | A tenant key revoked during an outage keeps matching for up to the window | | Outage retry interval | 5 s | none | One request timeout per read during an outage | A recovered API is seen up to 5 s late | | Cache size | 10,000 entries | TEMPER_RUNTIME_RESOLVE_CACHE_MAX_ENTRIES (min 1) | Unbounded memory, and live tenants evicted into a re-fetch herd; when full, it evicts an expired entry first, then the least recently used fresh one, and a stale-window entry (the only copy an outage can serve) last | Memory, well under 50 MB at a few KB per record | | Miss TTL (select) | 10 s | TEMPER_RUNTIME_RESOLVE_NOT_FOUND_MAX_AGE (0 off) | Retry bursts with one wrong key: each distinct wrong key costs one API call per 10 s | A tenant created after a miss is found up to 10 s later (at once with cache: false) | | Miss cap (select) | 60 per minute | TEMPER_RUNTIME_RESOLVE_MISS_CAP_PER_MINUTE (0 off) | A caller replaying unknown keys: once 60 distinct keys got a 404 this minute, those 404s and each later one stay cached until the minute ends, so a refused key is not sent again while the bounded miss cache keeps it; one warning per minute | A key the API refused during that minute, such as a tenant created just after its 404, is found when the minute ends (at once with cache: false). A key the API has not answered is never refused, so distinct unknown keys still cost one call each | | Expired-token wait | 2 s | none | Every read stalling while the token provider cannot refresh | A read with an expired token fails as an outage after 2 s | | Fresh-token wait | 35 s | none | A failed read at pod start or after a 401 while the provider publishes a token | Such a read can wait up to 35 s | | Request timeout | 5 s | UNNBOUND_ENV_API_TIMEOUT | A hung resolver call | A slower answer fails, or is served stale | | Expired-entry sweep | 60 s | none | Expired entries holding memory | One scan of the cache per minute |

Partner Accessors

Each partner has its own accessor for connections and variables:

const partner = env.partners.get('PARTNER_KEY');

// Partner connections
partner.connections.get('CONNECTION_KEY');

// Partner variables
partner.variables.get('VARIABLE_KEY');

// Partner attributes
partner.attributes;

selectPartnerFromEvent

Middleware that sets the current partner from the request context.

selectPartnerFromEvent((c) => {
  // Return partner key from request context
  return c.request.params?.partnerId;
});

selectPartner

Utility to execute code in a partner context.

import { selectPartner } from '@ontemper/resources';

await selectPartner('PARTNER_KEY', async (partner) => {
  // Code runs with partner context
  const apiKey = partner.variables.get('API_KEY');
});

Connection Types

// Basic Auth
{
  type: 'basic-auth',
  config: {
    url: string,
    headers: { key: string, value: string }[],
    username: string,
    password: string,
  }
}

// Bearer Token
{
  type: 'bearer-token',
  config: {
    url: string,
    headers: { key: string, value: string }[],
    apiKey: string,
    apiKeyPlacement: 'authorization_bearer' | 'custom_header',
    apiKeyHeaderName?: string, // required when placement is 'custom_header'
  }
}

// OAuth 2.0
{
  type: 'oauth20',
  config: {
    url: string,           // token endpoint
    headers: { key: string, value: string }[],
    grantType?: 'client_credentials' | 'password', // defaults to 'client_credentials'
    bodyParams?: { key: string, value: string }[], // extra params merged into token request body
    clientId: string,
    clientSecret: string,
    username?: string,     // required for password flow
    password?: string,     // required for password flow
  }
}

// Request (custom headers, no auth)
{
  type: 'request',
  config: {
    url: string,
    headers: { key: string, value: string }[],
  }
}

Managed PostgreSQL limits and timeout monitoring

PostgreSQL pools open connections on demand, use at most five connections, and set a ten-second statement timeout. Managed placements use verified TLS and enforce the Resource grant in PostgreSQL. Revoking a grant ends active sessions. Statement timeouts emit a temper.database.query.timeout warning without SQL, parameters, or credentials. The query still returns its original PostgreSQL error, including queries on a client checked out with pool.connect().