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 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,
});
| Field | Type | Required | Description |
|---|---|---|---|
baseURL | string | Yes | Base faynoSync API URL used for update checks. Must be an absolute URL. |
edgeURL | string | No | Optional edge endpoint tried first for static JSON responses. |
timeoutMs | number | No | Per-request timeout in milliseconds. Defaults to 30000. |
fetch | typeof fetch | No | Custom fetch implementation for proxies, pooling, or logging. |
Notes:
baseURLis required and is validated as an absolute URL.- If
fetchis omitted, the SDK usesglobalThis.fetch(Node.js 18+ built-in). - The same
timeoutMsis 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
| Field | Type | Required | Description |
|---|---|---|---|
owner | string | Yes | Application owner username. |
appName | string | Yes | Application name. |
version | string | Yes | Current client version. |
channel | string | Conditional | Release channel value used by your faynoSync setup. |
platform | string | Conditional | Platform value used by your faynoSync setup. |
arch | string | Conditional | Architecture value used by your faynoSync setup. |
deviceId | string | No | Device 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
| Field | Type | Description |
|---|---|---|
updateAvailable | boolean | Whether an update is available for the request scope. |
updateUrl | string | Direct binary update URL (single artifact flow). Empty string when not present. |
packageUrls | readonly PackageUpdateURL[] | Package-specific update URLs extracted from dynamic response keys, sorted alphabetically by package name. |
changelog | string | Optional release notes or markdown changelog. |
critical | boolean | Indicates a critical update. |
isIntermediateRequired | boolean | Indicates whether an intermediate update is required. |
possibleRollback | boolean | Indicates rollback metadata from the server response. |
source | UpdateSource | Where the response came from: 'edge', 'api', or 'unknown'. |
rollout | RolloutInfo (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_debupdate_url_rpmupdate_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
| Value | Meaning |
|---|---|
'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
| Field | Type | Description |
|---|---|---|
percent | number | The rollout target percentage the server issued for the version. |
seed | string | Stable per-version seed used to bucket the device. |
bucket | number | null | Deterministic bucket in [0, 99], or null when no deviceId was supplied. |
eligible | boolean | Whether 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 Result | Fallback to baseURL API | Final source |
|---|---|---|
| HTTP 200 + valid JSON | No | 'edge' |
| Network error | Yes | 'api' when API succeeds |
| Timeout | Yes | 'api' when API succeeds |
| Invalid JSON | Yes | 'api' when API succeeds |
| HTTP 404 | Yes | 'api' when API succeeds |
| Any non-200 status | Yes | '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 (recommended)
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:
| Field | Type | Description |
|---|---|---|
updateAvailable | boolean | Whether to invoke the native updater. false lets you skip it entirely. |
feedURL | string | URL to pass to setFeedURL: the edge object when the edge served the feed, otherwise the API endpoint. |
source | UpdateSource | 'edge' or 'api'. |
url | string (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_darwinreads the JSON feed directly, sofeedURLis the edge object (or the API/checkVersionURL on a miss).squirrel_windowsreadsfeedURL/RELEASES. The SDK resolves the edge redirect response, strips the trailing/RELEASES, and returns the directory asfeedURL— pointing Squirrel.Windows at the CDN whereRELEASESand the.nupkglive. On an edge miss it falls back to the API/update/...base (which servesRELEASESand 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',
});
| Updater | Resulting URL |
|---|---|
squirrel_darwin | GET {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
| Field | Type | Required | Description |
|---|---|---|---|
reportKey | string | Yes | Report ingestion key, sent as Authorization: Bearer. |
deviceId | string | Yes | Device identifier, sent as X-Device-ID. |
appName | string | Yes | Application name. |
version | string | Yes | Client version. |
channel | string | Yes | Release channel. |
platform | string | Yes | Platform value. |
arch | string | Yes | Architecture value. |
event | ReportEvent | Yes | { type, reason } describing the event. |
details | Record<string, unknown> | No | Optional 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:
| Field | Type | Description |
|---|---|---|
status | string | Server status string (e.g. accepted). |
groupHash | string | Grouping hash the server assigned to the report. |
storedDetails | boolean | Whether 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:
macostodarwinosxtodarwinstabletodefault
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
| Error | Trigger |
|---|---|
ErrMissingBaseURL | baseURL is empty. |
ErrInvalidBaseURL | baseURL is not a valid absolute URL. |
ErrInvalidEdgeURL | edgeURL is set but is not a valid absolute URL. |
ErrMissingOwner | opts.owner is empty. |
ErrMissingAppName | opts.appName is empty. |
ErrMissingVersion | opts.version is empty. |
ErrMissingChannel | opts.channel is empty (native feeds and reports). |
ErrMissingPlatform | opts.platform is empty (native feeds and reports). |
ErrMissingArch | opts.arch is empty (native feeds and reports). |
ErrMissingReportKey | reportKey is empty. |
ErrMissingDeviceId | deviceId is empty (reports). |
ErrInvalidEventType | event.type is not a supported report event type. |
ErrInvalidReason | event.reason does not match ^[a-zA-Z0-9._-]{1,128}$. |
Examples
Runnable examples live in the faynosync-sdk-js repository:
| Example | Description | Source |
|---|---|---|
| Basic | Minimal update check using runtime platform/arch detection | examples/basic |
| Edge fallback | edgeURL configured with deviceId telemetry | examples/edge-fallback |
| Custom fetch | Custom fetch function and timeoutMs | examples/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.