Codync
A free, open-source 1:1 alternative to Grok Bot and Muse. Message Claude Code, Codex, Cursor and 40+ coding agents as persistent bots from your iPhone, Mac or Linux desktop. Group chats, threads, appr
Documentation for @qualithm/device
Translation pending
Developer toolsTypeScript · PythonGitHub @qualithmOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Device provisioning and connectivity SDK for JavaScript and TypeScript runtimes.
Taken from the repository README
Loading the full README.
Read the full README ↗Device provisioning and connectivity SDK for JavaScript and TypeScript runtimes. It hides the device
lifecycle behind a single connect() call — claim once, persist the credential, and maintain an
auto-reconnecting MQTT-over-TLS session — on Node, Bun, and Deno.
Not for deployed devices. The SDK keeps the device credential in a file, and anyone with access to the device can read it. Don't deploy a device built on this SDK until it supports protected credential storage.
connect() — claim a device, persist its credential, and open an MQTT-over-TLS
session in a single call.fsync-backed persistence with a pluggable backend for
constrained or hardened targets.command/#,
decodes the payload, and routes a typed value.bun add @qualithm/device
# or
npm install @qualithm/device
import { Device } from "@qualithm/device"
const device = new Device({
provisioningUrl: "https://api.qualithm.com",
claimCode: process.env.QUALITHM_CLAIM_CODE,
capabilities: [{ key: "temperature", type: "sensor", metric: "temperature", unit: "celsius" }]
})
device.onState((state) => console.log("state:", state))
await device.connect()
await device.publish(
"telemetry",
JSON.stringify({ ts: Date.now(), metrics: { temperature: 21.4 } })
)
The platform stores readings published to the telemetry topic as { ts, metrics }: ts is the
reading time in epoch milliseconds, and metrics maps each metric name to a number. Other topics
reach the gateway but aren't stored as readings.
On first boot the SDK exchanges the claim code at POST /v1/provision/claim and persists the
returned credential. On every subsequent boot it loads the stored credential and skips claiming —
claim codes are single-use, so a power cycle never re-claims.
Set provisioningUrl to the API of the environment the claim code came from:
| Environment | provisioningUrl |
|---|---|
| Production | https://api.qualithm.com |
| Test | https://api.test.qualithm.com |
You don't configure the MQTT gateway. The claim response names the gateway for the device's zone,
and the SDK stores it with the credential, so every later boot connects to the same gateway.
GET /zones on the same API lists each zone's gateway.
To connect somewhere else, such as a local broker, set broker.host (and broker.port). A
configured host always wins over the claimed one. A credential stored by an SDK version before the
claimed broker has none, so it needs broker.host or a fresh claim.
connect() — inspects persisted state and only claims when no credential exists;
otherwise it connects directly.fsyncs, then
atomically renames, so a power loss mid-write cannot corrupt the credential.reconnectPeriodMs (1 s) up to 30 s, and resets once connected, so a fleet
never retries in lockstep. Subscriptions are re-established on resume.publish() rejects. Each QoS 1 message carries a msg-id user property that stays the same
across its attempts, so the gateway stores it once. Set your own msg-id to choose the id.CredentialStore (flash/NVS, secure element, keychain)
for constrained or hardened targets.The device generates its own key pair and CSR; an operator mints the certificate, which the device then stores and connects with:
import { generateDeviceCsr } from "@qualithm/device"
const { privateKeyPem, csrPem } = await generateDeviceCsr(deviceId)
// Submit csrPem to the operator mint flow, then persist the returned
// certificate alongside privateKeyPem as a `cert` credential.
Every device declares what it can do, with at least one capability: new Device throws a
CapabilityError without one. Dashboards and automations read the declaration, and the platform
validates every command against it before sending it. The manifest is published on every connect.
Register a handler per commandable key:
import { Device } from "@qualithm/device"
const device = new Device({
provisioningUrl: "https://api.qualithm.com",
claimCode: process.env.QUALITHM_CLAIM_CODE,
capabilities: [
{ key: "power", type: "onoff" },
{ key: "brightness", type: "range", min: 0, max: 100, unit: "percent" },
{ key: "reboot", type: "trigger" }
]
})
device.onCommand<boolean>("power", (value) => setRelay(value))
device.onCommand<number>("brightness", (value) => setBrightness(value))
device.onCommand("reboot", () => restart()) // a trigger arrives with no value
await device.connect() // publishes the manifest and subscribes command/#
A command arrives on command/<key> with a JSON object payload — {"value": ...}, or {} for a
trigger. A malformed payload or a value that does not fit the declared capability is reported
through onError and never reaches the handler. To change the capability set at runtime, call
device.declareCapabilities([...]) again.
For onboarding without a terminal — the companion-app flow — the device serves the claim exchange
itself. While no credential is stored, startProvisioning() brings up the setup access point
through an AccessPointController and serves the exchange on it. On a NetworkManager host,
createNmcliAccessPoint is that controller. A successful claim persists the credential, drops the
AP, and hands off to connect():
import { createNmcliAccessPoint, Device } from "@qualithm/device"
const device = new Device({
provisioningUrl: "https://api.qualithm.com",
name: "field-gateway",
capabilities: [{ key: "temperature", type: "sensor", metric: "temperature", unit: "celsius" }]
})
await device.startProvisioning({
accessPoint: createNmcliAccessPoint({ ssid: "qualithm-setup-field-gateway" }),
onProvisioned: () => device.connect()
})
The companion app joins the setup network, reads GET /provision/info, and posts the claim code to
POST /provision/claim. The server never runs alongside a gateway session: it refuses to start once
a credential exists, and a successful claim stops it before the MQTT session opens. A failed claim
(bad code, unreachable platform) leaves the server running, so onboarding can be retried.
A single-radio device can't reach the platform while it hosts the setup network. Supply a
HomeNetworkController as homeNetwork, and the companion app sends the home Wi-Fi ssid and
passphrase with the claim code. The device answers 202 { "status": "joining" }, drops the access
point, joins the home network, then claims from there. If the join or the claim fails, it forgets
the network and brings the access point back, so onboarding restarts. The companion app learns the
device's identity from the platform, not from the setup network.
All errors extend QualithmDeviceError; each subclass exposes a static isError() for
instanceof-free narrowing.
import { ClaimError, CredentialError } from "@qualithm/device"
try {
await device.connect()
} catch (error) {
if (CredentialError.isError(error)) {
// missing or unreadable credential
} else if (ClaimError.isError(error)) {
// claim code rejected or endpoint unreachable
} else {
throw error
}
}
Full API documentation is generated with TypeDoc:
bun run docs
# Output in docs/
See the examples/ directory for runnable examples:
| Example | Description |
|---|---|
basic-usage.ts | Configure a device, generate a CSR, claim + connect |
error-handling.ts | Typed error hierarchy and isError() narrowing |
credential-persistence.ts | Crash-safe file store; reuse the credential on restart |
softap-provisioning.ts | Serve the claim exchange on the device's setup network |
bun run examples/basic-usage.ts
bun install
bun run build
bun run test # unit tests
bun run test:integration # integration tests
bun run test:coverage # with coverage report
bun run lint
bun run format
bun run typecheck
bun run bench
Apache-2.0