v2.2.0

Environment

Runtime environment detection — getEnvironment(), isBrowserLikeRuntime() and the Environment enum for branching between browser, worker and Node.js code paths.

Overview

getEnvironment() reports whether the code is running in a browser, in a worker, in Node.js, or in an environment the SDK cannot classify. It returns a member of the Environment enum. Both are exported from @bitrix24/b24jssdk and are used internally to gate browser-only behaviour (DOM, localStorage, iframe messaging) away from server contexts.

import { getEnvironment, Environment } from '@bitrix24/b24jssdk'

if (getEnvironment() === Environment.BROWSE) {
  // browser-only code (DOM, localStorage, …)
}

Method Signature

enum Environment {
  UNKNOWN = 'unknown',
  BROWSE = 'browser',
  WORKER = 'worker',
  NODE = 'node'
}

getEnvironment(): Environment
isBrowserLikeRuntime(): boolean

Key Concepts

  • Detection order. getEnvironment() first checks for a browser by testing window and window.document; if both are present it returns Environment.BROWSE. Then Node, via process.versions.node. Only then does it ask whether this scope is a worker scope — globalThis instanceof WorkerGlobalScope — returning Environment.WORKER; anything left is Environment.UNKNOWN.
  • Node is tested before the worker, and the trade has a cost on both sides. The runtimes that report both — a Deno worker, and a Cloudflare Worker with nodejs_compat — are servers, where running a webhook is legitimate; calling them browser-like would keep credentials out of headers for no reason and warn on every call that a private secret had leaked. What it costs is the other direction: a browser worker that reports a Node version is read as a server. That is reachable — process@0.11.10 leaves process.versions empty, but unenv, the polyfill behind Nitro and Nuxt, answers { node: '22.14.0' } — and what it costs there, worst first: TelegramHandler would send, putting the bot token in a URL from code anyone can read; an Authorization header on a restApi:v3 OAuth batch, which the portal's preflight refuses (a path already documented as not working from a browser); a missing client-side warning; and a User-Agent the browser drops anyway. Detection is not a substitute for not shipping a secret to the client.
  • It is an instanceof, not a "is the name defined" check — the specification puts that constructor in a real worker's prototype chain, so asking whether this scope is one is the precise question. It is narrower than a name check, not airtight: Cloudflare's workerd does have WorkerGlobalScope in its chain and escapes only because it exposes a second, non-identical constructor as the global binding. With nodejs_compat and a recent compatibility date it reports a Node version and is classified as NODE; on an older date, or without nodejs_compat, it is UNKNOWN. Neither is browser-like, which is the outcome that matters.
  • A Bun Web Worker reports NODE, because it reports a Node version and Node is tested first — and Bun defines no WorkerGlobalScope anywhere either way. Not the right name for it, and harmless: nothing there enforces the browser rules this member exists to respect.
  • The browser member is BROWSE, its value is 'browser'. Compare against the enum member (Environment.BROWSE), not a bare string, so a rename stays type-safe.
  • BROWSE means there is a DOM — a worker is not BROWSE. A worker has no window.document, so code guarded on BROWSE must not run there. But the browser's other rules do apply in one: CORS, forbidden request headers, and a bundle anyone can read. isBrowserLikeRuntime() is the question to ask when that is what matters — it is true for both BROWSE and WORKER, and it is what the SDK's own transport uses to decide that it must not send an Authorization header or a User-Agent.
  • UNKNOWN covers everything else. Edge/serverless runtimes and other hosts that satisfy none of the three tests fall through to Environment.UNKNOWN — treat it as "assume no browser globals". A Cloudflare Worker without nodejs_compat is one of them.

Error Handling

getEnvironment() never throws. The window and process checks are typeof guards, so a missing global degrades to the next branch rather than raising a ReferenceError. The worker check is an instanceof, which a global that is not a constructor makes throw a TypeError — it is wrapped in a try / catch that answers false, so an odd runtime cannot turn this into a crash on import.

Examples

Branch between browser, worker and Node.js:

import { getEnvironment, Environment } from '@bitrix24/b24jssdk'

switch (getEnvironment()) {
  case Environment.BROWSE:
    // Safe to touch the DOM / localStorage here
    break
  case Environment.WORKER:
    // No DOM here — but the browser's rules still apply, so treat this code as
    // public. See isBrowserLikeRuntime() below.
    break
  case Environment.NODE:
    // Server-side path (e.g. read env vars, use a webhook client)
    break
  default:
    // Environment.UNKNOWN — an unrecognised host; avoid browser and Node-specific globals
    break
}

Tell a missing DOM from a missing browser:

import { getEnvironment, isBrowserLikeRuntime, Environment } from '@bitrix24/b24jssdk'

// "Can I touch the DOM?" — false in a worker.
const hasDom = getEnvironment() === Environment.BROWSE

// "Do the browser's rules apply?" — true in a worker, and the question to ask
// before shipping a secret or setting a forbidden header.
const isPublicCode = isBrowserLikeRuntime()

Guard browser-only work before it runs:

import { getEnvironment, Environment } from '@bitrix24/b24jssdk'

function isBrowser(): boolean {
  return getEnvironment() === Environment.BROWSE
}

if (isBrowser()) {
  // window / document are available
}

Alternatives and Recommendations

  • Use getEnvironment() for a coarse "where am I running" branch. For finer browser/OS/capability checks (Chrome vs Safari, iOS, touch, localStorage availability) use Browser instead.
  • Prefer it over ad-hoc typeof window checks. Centralising the detection keeps browser/Node branching consistent and matches the checks the SDK itself makes internally.
  • Ask isBrowserLikeRuntime(), not === BROWSE, for anything but the DOM. A worker is not a server: a webhook secret bundled into one is as public as on the main thread, and a header the browser forbids is dropped there too.
  • A runtime reported as WORKER is treated as public code. If yours is a server that merely exposes the Worker API, that would mean a credential kept out of headers, no User-Agent, and a "this webhook is client-side" warning on every call — which is why the check asks whether the global is a worker scope rather than whether the name exists. If you meet a runtime where it still answers wrongly, please open an issue.
  • Treat UNKNOWN conservatively. When you cannot confirm a browser, avoid DOM and localStorage access — many Browser methods assume those globals exist and can throw outside a browser.