# @dbx-tools/shared-core

Use browser-safe utilities shared by `@dbx-tools/*` packages in browsers,
workers, Node, Bun, CLIs, and schema packages. The package includes logging,
options, networking, JSON, hashing, predicates, and other dependency-light
foundations.

Import the namespaces that your code uses:

```ts
import {
  asyncUtils,
  brandUtils,
  environmentUtils,
  errorUtils,
  execution,
  functionUtils,
  hash,
  http,
  json,
  log,
  net,
  object,
  options,
  pattern,
  predicate,
  stringUtils,
  token,
} from "@dbx-tools/shared-core";
```

Node-only helpers live in [@dbx-tools/core](../../node/core). AppKit and
Databricks SDK helpers live in [@dbx-tools/appkit](../../node/appkit).

Use these utilities for:

- Abort-aware async utilities for polling, sleeping, and connecting cancellation
  across web and Node runtimes.
- Error normalization for unknown thrown values, nested causes, and HTTP-ish
  status/message extraction.
- Deterministic non-cryptographic hashes and short IDs for cache keys, slugs,
  and generated names.
- Non-throwing JSON parsing for untrusted input, with record narrowing so parsed
  data is not cast blindly.
- String normalization helpers for slugs, identifiers, unique labels, human
  labels, config lists, and prompt descriptions.
- Object/iterable, predicate, HTTP, cookie, network, token, memoization, and
  logging helpers that avoid Node-only dependencies.
- Namespace exports that make utility call sites explicit without creating a
  grab-bag default import.
- A Zod-backed `BrandContext` contract with dbx-tools defaults, JSON Schema
  output, and prompt serialization for browser, library, and LLM consumers.
- Shared Zod option schemas for Databricks environment names, normalized URLs,
  TCP ports, listener addresses, and schema-driven parse/serialization.

## Runtime boundary

This package doesn't read files, discover Databricks credentials, or call
workspace APIs. Node-only configuration and filesystem behavior lives in
[@dbx-tools/core](../../node/core). AppKit integration lives in
[@dbx-tools/appkit](../../node/appkit).

## Brand context

```ts
import { brandUtils } from "@dbx-tools/shared-core";

const context = brandUtils.parseBrandContext({ name: "Acme Data" });
const jsonSchema = brandUtils.brandContextJsonSchema();
const instructions = brandUtils.brandContextPrompt(context);
```

`BrandContextSchema` validates identity, theme-aware assets, colors,
typography, links, audience, and voice. Required fields have dbx tools defaults;
`tagline` remains absent unless the brand supplies it. Use
[@dbx-tools/core](../../node/core)
to discover and read YAML/JSON files, and
[@dbx-tools/ui/branding](../../ui/appkit) to apply the same context to a UI.

## Control asynchronous work

```ts
for await (const status of asyncUtils.poll(fetchStatus, {
  intervalMs: 250,
  timeoutMs: 30_000,
  predicate: (s) => s !== "READY",
})) {
  render(status);
}

await asyncUtils.sleep(500, abortSignal);
```

`asyncUtils.poll()` is useful for Databricks APIs that expose long-running state.
`asyncUtils.tieAbortSignal()` and `asyncUtils.sleep()` let route handlers connect caller
cancellation to background work.

## Handle errors

```ts
try {
  await run();
} catch (err) {
  logger.warn("run failed", { error: errorUtils.errorMessage(err) });
  const ctx = errorUtils.errorContext(err);
  return Response.json({ message: ctx.message }, { status: ctx.status ?? 500 });
}
```

`errorUtils.toError()`, `errorUtils.errorMessage()`, `errorUtils.errorMessages()`, and
`errorUtils.errorNodes()` normalize unknown thrown values. `errorUtils.errorContext()`
extracts HTTP-ish status/message detail from nested errors.

## Create hashes and IDs

```ts
const id = hash.id(8);
const cacheKey = hash.fnvHash("workspace", host, endpointName);
const suffix = hash.fnvHashWithOptions({ length: 6 }, longName);
```

These hashes are deterministic and non-cryptographic. Use them for cache keys,
slug suffixes, and trace-stable identifiers, not secrets or signatures.

## Parse untrusted JSON

```ts
const body = json.parseRecord(await readRequestText(req)) ?? {};
const chunk = json.parse<StreamChunk>(sseData);
if (!chunk) continue;

const settings = json.parse<Settings>(process.env.SETTINGS, DEFAULT_SETTINGS);
```

`json.parse()` returns the fallback (or `undefined`) instead of throwing, so a
malformed request body, env var, config file, subprocess stdout, or third-party
response does not need its own `try`/`catch`. `json.parseRecord()` also
narrows to `Record<string, unknown>`, which a bare `JSON.parse(...) as Record<...>`
does not: it rejects a parsed `null`, array, or scalar rather than letting it
through as an object.

Reach for `JSON.parse` directly only when a throw is the correct outcome, such as
reading a trusted generated file.

## Normalize strings and descriptions

```ts
const slug = stringUtils.toSlug("My Cool Project!");
const id = stringUtils.toIdentifierWithOptions({ delimiter: "_" }, "Model Name");
const unique = stringUtils.toUniqueSlug("Send Email", { fallbackPrefix: "tool" });
const script = stringUtils.dedent(`
  set -euo pipefail
  bun run build
`);
const description = stringUtils.toDescription([
  "Answer with SQL first.",
  { "When data is missing": "Say what is missing." },
]);
```

`stringUtils.tokenize()`, `toSlug()`, and `toIdentifier()` keep package names, tool
IDs, schema IDs, and generated labels consistent. `toDescription()` turns nested
description data into prompt/tool text without hand-concatenating paragraphs.
`dedent()` removes common indentation and surrounding blank lines from multiline
code or configuration templates. Its `trimStart` and `trimEnd` options preserve
boundary blank lines when generated output requires them, while `trimLineEnd:
false` preserves trailing spaces and tabs. `dedentLines()` returns the same
normalization as a line array for file generators.

Use the following helpers for common labels and configuration values:

```ts
const label = stringUtils.toLabel("web_search"); // "Web Search"
const name = stringUtils.capitalize(segment); // no charAt(0).toUpperCase() idiom
const host = stringUtils.trimToEmpty(parsed.host); // unknown JSON field -> string
const allowed = stringUtils.parseList(process.env.ALLOWED_URLS);
```

`toLabel()` and `capitalize()` are the humanizers for identifiers and path
segments. `trimToNull()` / `trimToEmpty()` / `firstNonEmpty()` coerce an unknown
field off parsed JSON. `parseList()` normalizes a config value that may arrive as
an array or as one comma/whitespace-separated env string, de-duplicating as it
goes.

## Work with objects and predicates

```ts
if (object.isRecord(value)) {
  const enabled = object.toBoolean(value.enabled);
}

const same = object.deepEqual(left, right);

const isRunnable = predicate
  .create((pkg: Package) => pkg.tags.includes("node"))
  .and((pkg) => pkg.name.includes("appkit"));
```

`object.deepEqual()` supports an optional comparator for domain-specific
short-circuits. `predicate.create()` returns composable predicates with `and`,
`or`, and `negate`, used heavily by the projen engine.

## Validate serializable values and stable identities

```ts
if (object.isSerializableValue(requestBody)) {
  await bus.broadcast("orders", { type: "order.updated", body: requestBody });
}

const key = object.toStableKey({ schema: "billing", version: 2 });
```

`object.isSerializableValue()` answers "does this survive a JSON round trip
unchanged", which is stricter than `JSON.stringify` not throwing: that succeeds
while turning a `Date` into a string, `NaN` into `null`, and a `Map` into `{}`,
and while dropping `undefined`. It narrows to `SerializableValue` instead of
throwing, so it doubles as the validator for a request body. Type a serializing
boundary as `SerializableValue` to catch the same mistakes at compile time.

`object.toStableKey()` canonicalizes a value so an identity can be derived from
it. Object key order does not affect the result, but types and structure do:
`1` and `"1"` differ, and so do `["a", "bc"]` and `["ab", "c"]`. It throws on a
cycle, a non-finite number, or a function/symbol rather than returning an
identity two callers could disagree about. `@dbx-tools/postgres` derives both its
advisory-lock IDs and its notification channel names through it. Use
`hash.fnvHash()` instead when a short collision-tolerant digest is enough. Its
canonicalizer is looser and folds every `Date` onto one token.

## Parse numbers, dates, and durations

```ts
object.toNumber("1,000"); //     1000
object.toNumber(" -2.5 "); //    -2.5
object.toNumber("12.5 %"); //    0.125
object.toNumber(""); //          undefined - `Number("")` would be 0
object.toNumber("12px"); //      undefined

object.toDate("2026-08-02"); //  a date or ISO instant
object.toDate("1785697899"); //  epoch SECONDS (not the year 1785697899)
object.toDate(1785697899000); // epoch millis
object.toDate("30 days ago"); // relative to now
object.toDate("now");
object.toDate("nope"); //        undefined - never throws

object.toDuration("1 hour 30 minutes"); // 5_400_000
object.toDuration("2ms"); //               2
object.toDuration("-7d"); //               -604_800_000
object.toDuration("2026-08-02"); //        ms from now until that instant
```

These helpers parse values entered in environment variables, CLI flags, and
configuration files. Whitespace between the amount and unit is
optional, units are case-insensitive, and plurals and abbreviations are
equivalent (`2ms` === `2 milliseconds`, `1h` === `1 hr` === `1 Hour`). Signs make
a duration an OFFSET, which is what lets `toDate` read `-30d` / `12 hours ago` /
`in 45s` as instants.

`toNumber` is the base the other two are built on, and the reason to reach for it
over `Number(value)` is that `Number` invents values: `""`, `null`, `[]`, and a
whitespace string all become `0`, so every caller has to re-check the result.
`toNumber` returns `undefined` instead, and accepts the spellings a person
actually types, including digit-group separators, a bare fraction, a trailing point, an
exponent, and a trailing percent. `ToNumberOptions` disables the `separators` and
`percent` leniencies for a string whose other characters carry meaning. A SQL
cell uses that so `"1,000"` in a text column stays the string the query returned.

`toDate` exists rather than `new Date(value)` because a bare epoch usually
arrives as a STRING (`date +%s`, a JSON field, a copied log line) and
`Date.parse("1785697899")` reads that as a YEAR, landing 1.7 billion years out.
Numeric input is therefore routed to the epoch path, with values under `1e11`
read as seconds and the rest as milliseconds. An unknown unit fails the whole
duration parse instead of being skipped, so `1 fortnight` is `undefined` rather
than `1`. Neither function throws. Like `toBoolean`, they return `undefined`, so
the caller decides whether a bad value is fatal, a warning, or a fallback
(`@dbx-tools/tunnel`'s `TUNNEL_AUTH_SESSION_CUTOFF` warns and carries on).

### Dates and durations are inverses

Each function falls back to the other, so one field accepts both readings: a
duration reaching `toDate` resolves against now, and a date reaching
`toDuration` becomes the signed offset from now (`date - now`, so a past instant
is negative). Each tries its own reading first. `toDate` runs `Date.parse`
before the duration fallback and recurses with the other's parser off, so a
value cannot bounce between them.

```ts
object.toDate("30 days ago", { parseDuration: false }); //  undefined
object.toDuration("2026-08-02", { parseDate: false }); //    undefined
```

Turn the fallback off where only one reading can be correct. A stored timestamp
or an `expires_at` field must not accept a duration, because resolving against
the current clock makes the same input mean something different on every call. A
timeout or a poll interval must not accept a date, because it would yield a
plausible but wrong number that also drifts.

## Transform iterables

```ts
const names = object
  .sequence(packages)
  .map((p) => p.name)
  .filter(Boolean)
  .distinct()
  .toArray();

const grouped = object.group(packages, {
  node: (p) => p.tags.includes("node"),
  ui: (p) => p.tags.includes("ui"),
});
```

The iterable helpers are lazy and work well for filesystem scans, package lists,
and one-pass generated data. Use `sequence(..., { cache: true })` when a source
must be re-read.

## Read HTTP headers and cookies

```ts
const cookies = http.parseCookies(req);

let bearer: string | undefined;
http.forEachHeaderValue(req, "authorization", (value) => {
  if (value.startsWith("Bearer ")) bearer = value.slice("Bearer ".length);
});
```

`http.HeaderLike` works with Fetch `Request`, Express-ish requests, Node header
records, and plain `{ headers }` objects. `http.createFetchError()` turns a
failed `Response` into an error with response text attached.

## Run intercepted execution

`execution.directExecutor()` gives plugin-backed tools a no-plugin fallback with
the same success/failure shape as AppKit's executor. `execution.run()` merges the
plugin timeout signal with the caller's cancellation signal and centralizes
result unwrapping without imposing an AppKit dependency on shared code.

## Parse network strings, email addresses, and CIDR blocks

```ts
const url = net.urlBuilder("example.com")?.withPathAppend("api", "2.0");
const emails = net.parseEmails("alice@example.com; bob@example.com");
const cidr = net.parseCidr("10.0.0.0/8");
const internal = cidr ? net.ipInCidr("10.1.2.3", cidr) : false;
```

`net.urlBuilder()` is a forgiving URL builder for config and REST helpers.
`urlBuilder(...).pathMatches()` compares path prefixes on segment boundaries.
IP/CIDR helpers parse IPv4 and IPv6 into a shared bigint comparison model.

## Compile allowlist patterns

```ts
// One matcher from a config array or a delimited environment string.
const forwardable = pattern.toPatternMatcher(["x-mastra-*", "/^x-trace-/", "x-tenant"]);

forwardable("x-mastra-model"); // true  (glob)
forwardable("x-trace-parent"); // true  (regex literal)
forwardable("x-tenant"); // true  (literal, whole-string)
forwardable("x-forwarded-user"); // false
```

Configurable allowlists accept the same three shapes, so the
compilation lives here once: a `/regex/` literal (with optional flags), a
shell-style glob (`*`, `?`, anchored at both ends, all other characters escaped),
or a literal compared as a whole string. Matching is case-insensitive by
default, which suits HTTP header names and email addresses. Set `caseSensitive`
to opt out.

An invalid regex is skipped with a warning instead of throwing, so one bad
config entry cannot stop a process from starting. No usable patterns yields a
matcher that always returns `false`. A caller that reads "no patterns" as "permit
everything" must say so itself. The result is a `predicate`, so it composes with
`.and()` / `.or()` / `.negate()`.

Use [@dbx-tools/path](../../node/path)'s `toPathMatcher` instead when
matching a filesystem path or URL path, where `/` is a segment boundary and `**`
is meaningful. That matcher uses `minimatch`. This module remains
dependency-free so it stays usable in a browser bundle.

## Read token claims

```ts
const scopes = token.getAccessTokenScopes(req, "x-forwarded-access-token");
const canReadWorkspace = token.includesAccessTokenScope(scopes, ["workspace", "all-apis"]);
```

Token helpers decode JWT payloads without validating signatures. Use them for
request-scoped authorization hints after the platform has already authenticated
the request.

## Memoize functions

```ts
const getRanges = functionUtils.memoize(fetchRanges, {
  ttlMs: 24 * 60 * 60 * 1000,
});
```

`functionUtils.memoize()` caches sync or async factories, evicts rejected
promises, and supports TTL-based refresh. It is useful for public metadata feeds,
SDK catalogs, and expensive computed constants.

Serializing whole callbacks rather than caching one value is a Node concern
(it needs `node:worker_threads`), so it lives in
[@dbx-tools/core](../../node/core)'s `processLock` instead.

## Write logs

```ts
const logger = log.logger("mastra/genie");
logger.info("space:resolved", { spaceId });

if (log.isLevelEnabled("debug")) {
  logger.debug("large payload", expensivePayload());
}
```

`log.logger()` is dependency-free and uses the platform console in browsers or
formatted process streams on server runtimes: debug/info use stdout and
warn/error use stderr. It honors `LOG_LEVEL` per call, so debug statements can
stay in production code without paying formatting cost when disabled.

## Package exports

- `asyncUtils`: polling, sleep, and abort-signal wiring.
- `errorUtils`: unknown-error normalization and HTTP-ish error context.
- `hash`: IDs, FNV hashes, and base32 encoding.
- `json`: non-throwing `parse()` and record-narrowing `parseRecord()`.
- `stringUtils`: tokenization, slugs, identifiers, human labels, string coercion,
  config lists, multiline string/line dedenting, descriptions, pluralization,
  and HTML escaping.
- `object`: record checks, number/boolean/date/duration coercion, present-only
  field spreading, deep equality, JSON-round-trip guards
  (`isSerializableValue`), canonical identity keys (`toStableKey`), shape types,
  and lazy sequence transforms + collection helpers.
- `predicate`: composable boolean/type predicates.
- `pattern`: literal, glob, and `/regex/` allowlist matching compiled to a
  predicate.
- `http`: header iteration, cookie parsing, and fetch error creation.
- `execution`: direct executor fallback, cancellation merging, and result unwrapping.
- `net`: URL building, email parsing, path matching, and IP/CIDR helpers.
- `options`: Databricks environment/config schemas, ports, listener addresses,
  URL normalization, nullable map parsing, and flag/env JSON serialization.
- `token`: JWT payload and scope readers.
- `functionUtils`: memoization.
- `log`: tagged leveled logging.
- `brandUtils`: Zod schema, defaults, JSON Schema, and LLM prompt serialization.
- `environmentUtils`: Databricks App runtime detection from App env vars or the
  Apps container source tree.
