Skip to main content

JavaScript SDK

Production-oriented JavaScript/TypeScript SDK for checking application updates with faynoSync.

This package is a small typed transport and developer experience layer. It does not implement update installation, platform normalization, metadata verification, caching, or business rules.

Edge delivery requires an SDK

Edge response caching and the edge-first fallback flow are designed to work correctly when clients use an official faynoSync SDK (this JS SDK or the Go SDK). Raw /checkVersion calls do not provide the same edge orchestration, source semantics, or SDK-optimized telemetry paths. See the SDK overview for context.

Requirements

Node.js 18 or later. The SDK uses the built-in global fetch and AbortSignal.timeout.

Installation

npm install @faynosync/sdk-js

Quick Start

import { Client } from '@faynosync/sdk-js';

const client = new Client({
baseURL: 'https://api.example.com',
});

const resp = await client.checkForUpdates({
owner: 'admin',
appName: 'test',
version: '0.0.0.5',
channel: 'nightly',
platform: 'darwin',
arch: 'arm64',
});

if (resp.updateAvailable) {
if (resp.updateUrl !== '') {
console.log('Update is available:', resp.updateUrl);
}
for (const pkg of resp.packageUrls) {
console.log(`${pkg.package} update is available:`, pkg.url);
}
}

Client Configuration

import { Client } from '@faynosync/sdk-js';

const client = new Client({
baseURL: 'https://api.example.com',
edgeURL: 'https://cdn.example.com',
timeoutMs: 10_000,
fetch: customFetch,
});
FieldTypeRequiredDescription
baseURLstringYesBase faynoSync API URL used for update checks. Must be an absolute URL.
edgeURLstringNoOptional edge endpoint tried first for static JSON responses.
timeoutMsnumberNoPer-request timeout in milliseconds. Defaults to 30000.
fetchtypeof fetchNoCustom fetch implementation for proxies, pooling, or logging.

Notes:

  • baseURL is required and is validated as an absolute URL.
  • If fetch is omitted, the SDK uses globalThis.fetch (Node.js 18+ built-in).
  • The same timeoutMs is applied to every request.
  • The client is safe for concurrent use.

checkForUpdates Contract

checkForUpdates sends a request and resolves to a typed UpdateResponse. An optional AbortSignal can be passed as the second argument to cancel the request.

const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);

const resp = await client.checkForUpdates(opts, controller.signal);

The request also respects timeoutMs: whichever fires first — your signal or the timeout — aborts the request.

CheckOptions

FieldTypeRequiredDescription
ownerstringYesApplication owner username.
appNamestringYesApplication name.
versionstringYesCurrent client version.
channelstringConditionalRelease channel value used by your faynoSync setup.
platformstringConditionalPlatform value used by your faynoSync setup.
archstringConditionalArchitecture value used by your faynoSync setup.
deviceIdstringNoDevice identifier. Sent as the X-Device-ID header, triggers a telemetry beacon after a successful edge response, and is required to participate in staged rollouts.

The SDK only validates owner, appName, and version as required; omitted fields are sent as empty query values. channel, platform, and arch are not validated client-side, but if the target app in your faynoSync instance defines channels, platforms, or architectures, you must provide the matching values or the check will not resolve to the correct release.

Request Mapping

Base API endpoint:

GET /checkVersion?app_name=test&version=0.0.0.5&channel=nightly&platform=darwin&arch=arm64&owner=admin&updater=manual
X-Device-ID: optional-device-id

The SDK's own update check always sends updater=manual. Framework-native updaters use different updater values — see Native Updater Feeds.

Query parameters are built from the named fields on CheckOptions. You pass a typed object instead of a generic key-value map, which keeps the API explicit and easy to validate.

Response Contract

checkForUpdates decodes the server response into a typed UpdateResponse.

Core Response Fields

FieldTypeDescription
updateAvailablebooleanWhether an update is available for the request scope.
updateUrlstringDirect binary update URL (single artifact flow). Empty string when not present.
packageUrlsreadonly PackageUpdateURL[]Package-specific update URLs extracted from dynamic response keys, sorted alphabetically by package name.
changelogstringOptional release notes or markdown changelog.
criticalbooleanIndicates a critical update.
isIntermediateRequiredbooleanIndicates whether an intermediate update is required.
possibleRollbackbooleanIndicates rollback metadata from the server response.
sourceUpdateSourceWhere the response came from: 'edge', 'api', or 'unknown'.
rolloutRolloutInfo (optional)Present only when the server offered a staged rollout for the version. See Staged Rollout.

A PackageUpdateURL has two fields:

interface PackageUpdateURL {
readonly package: string; // e.g. 'deb', 'rpm', 'msi'
readonly url: string;
}

Dynamic Package URL Mapping

faynoSync can return package-specific URL fields with dynamic suffixes, for example:

  • update_url_deb
  • update_url_rpm
  • update_url_msi

Other package types use the same update_url_<package> pattern (for example, update_url_appimage or update_url_pkg). The SDK maps every update_url_* string field into the typed packageUrls collection:

for (const pkg of resp.packageUrls) {
console.log(pkg.package, pkg.url);
}

Source Semantics

ValueMeaning
'edge'The response was served from the edge JSON endpoint.
'api'The response was served by the baseURL API, after a direct call or fallback.
'unknown'Default before the source is resolved; not expected on a returned response.

Staged Rollout

faynoSync can ship a version to a controlled percentage of the fleet first (a staged/canary rollout). The server stays stateless — it only issues a target percentage and a stable seed — and the SDK decides client-side whether a given install is included. See Staged Rollout for the full design.

When the offered version's rollout is below 100%, /checkVersion includes a rollout object:

{
"update_available": true,
"update_url": "https://downloads.example.com/app",
"rollout": { "percent": 20, "seed": "badadc23b08e3943" }
}

At 100% the field is omitted and the SDK treats the response as a normal full update. The decision is deterministic and sticky, using the reference algorithm shared by every faynoSync SDK:

bucket = sha256(deviceId + ":" + seed) → first 8 bytes, big-endian uint64, % 100
included if bucket < rollout.percent

When the install is not in the bucket, the SDK forces updateAvailable to false and blanks updateUrl/packageUrls, so a caller that checks the URLs instead of updateAvailable still cannot pull an update the device was not offered. A normal if (resp.updateAvailable) gate is all you need; the rollout object exposes the decision for logging:

const resp = await client.checkForUpdates({ ...opts, deviceId: 'stable-device-id' });

if (resp.rollout) {
console.log(resp.rollout.percent, resp.rollout.bucket, resp.rollout.eligible);
}

RolloutInfo

FieldTypeDescription
percentnumberThe rollout target percentage the server issued for the version.
seedstringStable per-version seed used to bucket the device.
bucketnumber | nullDeterministic bucket in [0, 99], or null when no deviceId was supplied.
eligiblebooleanWhether the install is inside the rollout. When false, the SDK has already gated the response.

deviceId is required to participate: it must be the same stable value used for telemetry (X-Device-ID). Without it the bucket cannot be computed, so the install stays out of the rollout (eligible: false, bucket: null) until a deviceId is provided. Raising the percentage on the same version only ever adds installs — an install in the 20% bucket stays in when you move to 50%.

A malformed rollout object (missing/non-numeric percent, or missing/empty seed) is ignored and treated as no rollout at all — the full update is served. Rollout works identically in edge/CDN mode, since the same JSON body is served from the cached manifest and the split happens in the SDK after the (cacheable) response is read.

The rolloutBucket(deviceId, seed) helper is exported if you need to compute a bucket yourself.

Edge Fallback Behavior

Edge delivery only works as intended when update checks go through an official faynoSync SDK. The server can publish cached JSON for CDN/object storage, but the client must implement edge-first requests, API fallback, and the optional telemetry flow that the SDK provides.

When edgeURL is configured, the SDK first requests:

GET /responses/{owner}/{appName}/{channel}/{platform}/{arch}/{updater}/{version}.json

Example (updater is manual for the SDK's own check):

GET /responses/admin/test/nightly/darwin/arm64/manual/0.0.0.5.json

In the edge path, - in the version is normalized to . (so 2.0.0-4 resolves to 2.0.0.4.json), matching the Base API behavior.

Fallback Decision Matrix

Edge ResultFallback to baseURL APIFinal source
HTTP 200 + valid JSONNo'edge'
Network errorYes'api' when API succeeds
TimeoutYes'api' when API succeeds
Invalid JSONYes'api' when API succeeds
HTTP 404Yes'api' when API succeeds
Any non-200 statusYes'api' when API succeeds

If the request was aborted, the SDK does not fall back. If both edge and API fail, checkForUpdates rejects with a CheckError.

Edge Telemetry

When deviceId is set and an edge response succeeds, the SDK sends a telemetry beacon to GET /telemetry/beacon (with is_latest derived from updateAvailable). Telemetry failures are swallowed and never affect the update-check result.

Native Updater Feeds

Framework-native updaters (Squirrel.Mac, Squirrel.Windows) poll faynoSync themselves with a different updater value and URL shape than the SDK's manual check. The SDK owns that wire format so you only pick the updater. Supported values are exported as NATIVE_UPDATERS:

import { NATIVE_UPDATERS } from '@faynosync/sdk-js';
// ['squirrel_darwin', 'squirrel_windows']

NativeFeedOptions extends CheckOptions with a required updater field. For native feeds owner, appName, version, platform, and arch are all required; an unknown updater throws UnsupportedUpdaterError.

resolveNativeFeed resolves the feed edge-first (falling back to the API) and tells you whether to invoke the native updater at all. When an edge response exists, the returned feedURL points at the CDN, so the native updater reads it directly and the API is never hit.

const feed = await client.resolveNativeFeed({
owner: 'admin',
appName: 'test',
version: '0.0.1',
channel: 'nightly',
platform: 'darwin',
arch: 'arm64',
updater: 'squirrel_darwin', // or 'squirrel_windows'
});

if (feed.updateAvailable) {
autoUpdater.setFeedURL({ url: feed.feedURL });
autoUpdater.checkForUpdates();
}

It resolves to a typed NativeFeedResult:

FieldTypeDescription
updateAvailablebooleanWhether to invoke the native updater. false lets you skip it entirely.
feedURLstringURL to pass to setFeedURL: the edge object when the edge served the feed, otherwise the API endpoint.
sourceUpdateSource'edge' or 'api'.
urlstring (optional)The raw resolved resource (the .zip for darwin, the RELEASES URL for windows).

Why go through the SDK: Squirrel.Mac expects 200 { "url": "<zip>" } or 204 No Content, but the edge mirror returns 200 { "status": "no_content" } (not 204) when there is no update, and 404 until the API has warmed that version's edge object. The SDK reads the edge response, returns updateAvailable: false on no_content so you skip the native updater, and falls back to the API (which warms the edge) on a miss. Pointing the native updater straight at the edge URL yourself would break on those cases.

feedURL differs by framework:

  • squirrel_darwin reads the JSON feed directly, so feedURL is the edge object (or the API /checkVersion URL on a miss).
  • squirrel_windows reads feedURL/RELEASES. The SDK resolves the edge redirect response, strips the trailing /RELEASES, and returns the directory as feedURL — pointing Squirrel.Windows at the CDN where RELEASES and the .nupkg live. On an edge miss it falls back to the API /update/... base (which serves RELEASES and warms the edge).

buildNativeFeedURL (low-level)

Builds just the feed URL — no request, no edge fallback — when you want to wire the native updater yourself:

const url = client.buildNativeFeedURL({
owner: 'admin',
appName: 'test',
version: '0.0.1',
channel: 'nightly',
platform: 'darwin',
arch: 'arm64',
updater: 'squirrel_darwin',
});
UpdaterResulting URL
squirrel_darwinGET {baseURL}/checkVersion?...&updater=squirrel_darwin
squirrel_windows{baseURL}/update/{owner}/{app}/{channel}/{platform}/{arch}/{version} (Squirrel.Windows appends /RELEASES itself)

Reports

reportEvent posts a failure or diagnostic report to POST /reports/ingest. The client stays stateless and app-agnostic, so reportKey and deviceId are passed per call (like checkForUpdates), not stored in Config.

const resp = await client.reportEvent({
reportKey: 'rpk_...', // sent as Authorization: Bearer
deviceId: 'device-1', // required, sent as X-Device-ID
appName: 'test',
version: '0.0.0.5',
channel: 'nightly',
platform: 'darwin',
arch: 'arm64',
event: { type: 'crash', reason: 'segfault.signal-11' },
details: { stack: '...', exitCode: 11 }, // optional raw debug object
});

console.log(resp.status, resp.groupHash, resp.storedDetails);

ReportOptions

FieldTypeRequiredDescription
reportKeystringYesReport ingestion key, sent as Authorization: Bearer.
deviceIdstringYesDevice identifier, sent as X-Device-ID.
appNamestringYesApplication name.
versionstringYesClient version.
channelstringYesRelease channel.
platformstringYesPlatform value.
archstringYesArchitecture value.
eventReportEventYes{ type, reason } describing the event.
detailsRecord<string, unknown>NoOptional raw debug object.

event.type must be one of crash, startup_failure, update_failure, install_failure, or rollback_failure. event.reason must match ^[a-zA-Z0-9._-]{1,128}$. All non-details fields are validated client-side.

details is optional. When supplied, the SDK serializes it with JSON.stringify, gzip-compresses it, and base64-encodes the result. The request carries it as:

{
"details": {
"encoding": "gzip+base64",
"content_type": "application/json",
"payload": "<base64(gzip(json))>"
}
}

When details is omitted, the field is left out of the request entirely.

A successful request returns HTTP 202 mapped to a typed ReportResponse:

FieldTypeDescription
statusstringServer status string (e.g. accepted).
groupHashstringGrouping hash the server assigned to the report.
storedDetailsbooleanWhether the server stored the details payload.

Any non-202 response rejects with an EndpointError whose source is 'report' and statusCode holds the HTTP status (e.g. 401, 403, 429). Network and JSON failures are wrapped in EndpointError as well. An optional AbortSignal can be passed as the second argument to cancel the request.

Platform, Channel, and Architecture Values

faynoSync supports custom values for platform, channel, and architecture. The SDK never normalizes or remaps them.

Examples of values that are not automatically transformed:

  • macos to darwin
  • osx to darwin
  • stable to default

Whatever you set in channel, platform, and arch is sent as-is.

Optional System Helpers

The SDK provides optional helpers that return the current Node.js runtime values:

import { systemPlatform, systemArch } from '@faynosync/sdk-js';

const platform = systemPlatform(); // process.platform — e.g. 'darwin', 'linux', 'win32'
const arch = systemArch(); // process.arch — e.g. 'arm64', 'x64'

These helpers are never called automatically. Use them only when Node.js runtime values match your faynoSync configuration.

Error Handling

The SDK exports a typed error hierarchy:

FaynoSyncError
├── ValidationError // missing/invalid configuration or options
│ └── UnsupportedUpdaterError // unknown native updater value
└── RequestFailedError
├── EndpointError // a single edge, API, or report request failed
└── CheckError // the overall check failed (wraps edge/api errors)

UnsupportedUpdaterError is thrown by buildNativeFeedURL / resolveNativeFeed for an unknown updater; narrow it with instanceof and read its updater field.

Validation errors are exported as singleton instances — compare them with ===. Request failures are class instances — narrow them with instanceof.

import {
Client,
CheckError,
EndpointError,
ErrMissingBaseURL,
ErrMissingOwner,
ErrMissingAppName,
ErrMissingVersion,
} from '@faynosync/sdk-js';

try {
const resp = await client.checkForUpdates(opts);
} catch (err) {
if (err === ErrMissingBaseURL) {
// configure Client baseURL
} else if (err === ErrMissingOwner) {
// set opts.owner
} else if (err === ErrMissingAppName) {
// set opts.appName
} else if (err === ErrMissingVersion) {
// set opts.version
} else if (err instanceof CheckError) {
// every edge/api attempt failed
console.error('edge error:', err.edgeError?.message);
console.error('api error:', err.apiError?.message);

if (err.apiError instanceof EndpointError) {
console.error('url:', err.apiError.endpointUrl);
console.error('status:', err.apiError.statusCode);
}
}
}

A failed update check always rejects with a CheckError. Its edgeError and apiError fields hold the underlying EndpointError for each attempt (the edge error is absent when edgeURL is not configured). Each EndpointError carries the source, endpointUrl, optional statusCode, and a cause for transport-level failures.

Validation Error Reference

ErrorTrigger
ErrMissingBaseURLbaseURL is empty.
ErrInvalidBaseURLbaseURL is not a valid absolute URL.
ErrInvalidEdgeURLedgeURL is set but is not a valid absolute URL.
ErrMissingOwneropts.owner is empty.
ErrMissingAppNameopts.appName is empty.
ErrMissingVersionopts.version is empty.
ErrMissingChannelopts.channel is empty (native feeds and reports).
ErrMissingPlatformopts.platform is empty (native feeds and reports).
ErrMissingArchopts.arch is empty (native feeds and reports).
ErrMissingReportKeyreportKey is empty.
ErrMissingDeviceIddeviceId is empty (reports).
ErrInvalidEventTypeevent.type is not a supported report event type.
ErrInvalidReasonevent.reason does not match ^[a-zA-Z0-9._-]{1,128}$.

Examples

Runnable examples live in the faynosync-sdk-js repository:

ExampleDescriptionSource
BasicMinimal update check using runtime platform/arch detectionexamples/basic
Edge fallbackedgeURL configured with deviceId telemetryexamples/edge-fallback
Custom fetchCustom fetch function and timeoutMsexamples/custom-fetch

Run any example with:

npx ts-node examples/basic/index.ts

For full desktop integrations wiring this SDK to real native updaters, see the Example Apps — Electron, Squirrel, and Tauri reference implementations.

Security Scope

This SDK version performs transport requests — update checks, native updater feed resolution, and failure/diagnostic report ingestion — with typed response decoding only.

It does not verify TUF metadata, signatures, thresholds, expiration, rollback protection, or cache safety. Applications that need secure update metadata verification must perform that verification in the appropriate faynoSync component or a future SDK layer that explicitly implements it.

No signature, threshold, expiration, rollback, freeze, root-of-trust, or cache protection is weakened by this transport-only SDK.