Enrol and watch your devices
Declare device, make an enrolment token, enrol a device with its own key, read its health, receive its telemetry once and in order, and revoke it.
Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
You will let a program on a machine enrol with your app, prove who it is with a key it made, report its health and send records that a handler of yours receives once and in order. Then you will revoke it. It takes about 20 minutes. Devices call your app; nothing here opens a port on a device.
You need
- A MainSDK app with a
main.yamland a signed-in operator who holdsdevice.manageanddevice.view. - A secret for the authority key that signs credentials: a PKCS#8 Ed25519 key as base64, stored as
MAINSDK_DEVICE_KEY. In development the SDK makes a key for you;mainsdk checkreportsMAIN-DEVICE-0016if production has none.
1. Declare the section and import the module
device:
authority: secret:MAINSDK_DEVICE_KEY
enrol: { proofs: [token] }
attest: { require: software }
heartbeat: { every: 60s, late-after: 2, silent-after: 5 }
telemetry:
streams:
app.status: { redact: [user.email] }import { device } from "mainsdk/device"; // not exported from "mainsdk"With no device section nothing is mounted, stored or timed, even when you import the module. Declaring the section without the import is MAIN-DEVICE-0002; importing without the section is MAIN-DEVICE-0001. The adapters serve device.handler() at device.path for you.
2. Make an enrolment token
import { device } from "mainsdk/device";
const { token } = await device.enrolments.create({ group: "lab", uses: 10 });The token is shown once and only its hash is stored. One token takes at most ten attempts a minute; past that a device gets MAIN-DEVICE-0017.
3. Enrol the device
import { deviceClient, fileKeyStore, softwareEvidence } from "mainsdk/device/client";
const client = deviceClient({
url: "https://app.example.com/mainsdk/device",
keys: fileKeyStore("/var/lib/acme"),
attest: softwareEvidence({ software: { name: "acme-agent", version: "2.1.0" } }),
});
await client.enrol({ token: "mde_paste-the-token-here" });
await client.start();
client.send("app.status", { ok: true });The key is made on the device and never leaves it; the key file has mode 0600 in a folder with mode 0700, and a readable key file is refused with MAIN-DEVICE-0015. The device joins the token's tenant as pending and becomes online at its first heartbeat.
4. Receive telemetry
import { device } from "mainsdk/device";
device.telemetry.on("app.status", async (records) => {
console.log(`received ${records.length} records`);
});A batch is acknowledged only after every handler resolves. A handler that throws makes the device resend the same batch with the same record ids, so write your handler to be safe to repeat. Records with a secret or personal data the stream did not allow are refused with MAIN-DEVICE-0034; the rest go on.
5. Read health
import { device } from "mainsdk/device";
const lab = await device.list({ group: "lab", state: "silent" });
const counts = await device.summary.get();A device is late after two missed heartbeats and silent after five; each silence raises one device.silent signal. Time your own app was down is left out, so an outage of yours never turns a fleet silent.
6. Revoke
import { device } from "mainsdk/device";
await device.revoke("dvc_01JC8Q4Z7N0ABCDEFGHJKMNPQR", { reason: "lost" });Every instance refuses the device from its next request with MAIN-DEVICE-0008 and a signed notice, and the client deletes its key. A revoked id is never reactivated; the device enrols again under a new id.
7. Test it
import { createTestApp, simulateDevices } from "@mainsdk/testing";
const app = await createTestApp({ yaml: "schema: 1\ndevice:\n driver: local\n" });
const sim = await simulateDevices(app, { count: 50 });
sim.offline(5);
await sim.run("10m");simulateDevices drives real clients on the fake clock, so a test of silence takes milliseconds. sim.tamper(id, "replay") breaks one device's next request and the refusal appears in sim.refused.
Check it
pnpm mainsdk checkThe check passes when the section is valid, the module is imported, every telemetry handler names a declared stream and production has an authority key.
Was this page helpful?