Connected devices
deviceOptional plumbing for any fleet of devices or agents your app manages - enrolment, device identity and attestation, signed telemetry, policy sync, approved commands and health - with nothing loaded until you declare it.
device-identityFree in your app, Apache-2.0Status 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.
Optional plumbing for any fleet of devices or agents your app manages - enrolment, device identity and attestation, signed telemetry, policy sync, approved commands and health - with nothing loaded until you declare it.
At a glance
device:
driver: local # local | taps (from @mainsdk/device-taps)
path: /mainsdk/device
authority: secret:MAINSDK_DEVICE_KEY
enrol: { proofs: [token, code] }
attest: { require: none }
telemetry: { streams: { app.status: {} }, max-batch: 1MB }
heartbeat: { every: 60s }
policy:
rollout: { stages: ["5%", "25%", "100%"], hold: 1h }
tighten: { report.interval: higher }
commands:
ttl: 15m
max-targets: 100
reaction-max: 50import { device } from "mainsdk/device"; // not exported from "mainsdk"
import { deviceClient, fileKeyStore } from "mainsdk/device/client";
const { token } = await device.enrolments.create({ group: "lab", uses: 10 });
device.telemetry.on("app.status", async (records) => { await store(records); });
// on the device
const client = deviceClient({ url: "https://app.example.com/mainsdk/device", keys: fileKeyStore("/var/lib/acme") });
await client.enrol({ token });
await client.start();
client.send("app.status", { ok: true });Calls
device.enrolments.create({ group, uses, expiresIn })device.enrolments.list({ cursor })device.enrolments.revoke(id, { reason })device.get(id)device.list({ group, state, cursor })device.update(id, { name, group, labels })device.revoke(id, { reason })device.handler()device.telemetry.on(stream, handler, { schema })device.summary.get({ group })device.policy.define(schema, { tighten })device.policy.publish(content, { target, reason, rollout })device.policy.get(version, { target })device.policy.list({ target, cursor })device.policy.rollback({ target, to, reason })device.policy.tighten({ target, path, value, expiresIn, reason })device.commands.define(type, { input, risk, approve, undo })device.commands.create(type, { to, input, reason })device.commands.get(id)device.commands.list({ device, state, cursor })device.commands.cancel(id, { reason })device.commands.revert(id, { reason })device.mcp.tools()deviceClient({ url, keys, dir })fileKeyStore(dir, { alg })memoryKeyStore({ alg })webCryptoKeyStore({ db })softwareEvidence({ software })simulateDevices(app, { count })deviceConformance({ driver })
Guarantees
Whatever the driver.
- Off until declared in main.yaml and imported; no route, table, timer or cost otherwise, and never re-exported from mainsdk
- Device keys are made on the device and never leave it
- Every device message is signed, fresh and bound to one tenant
- Telemetry is delivered once and in order, with redaction on the device and on the server
- A device keeps its last good policy when the app is unreachable and never accepts an older one
- Commands run only registered handlers, expire, and need people's approval when risky; an agent never approves
- No detection rules, endpoint event schemas or code delivery, now or later (ADR-0076)
Drivers
2 drivers. Select one to see its line in main.yaml.
device:
driver: localChange this one line to swap providers; your calls stay the same.Built-in and local drivers integration
Codes
| Code | Where it appears | Meaning |
|---|---|---|
MAIN-DEVICE-0001 | Checkmainsdk check, before deploy | mainsdk/device is imported but main.yaml has no device section; device calls throw it at runtime (500) |
MAIN-DEVICE-0002 | Checkmainsdk check, before deploy | main.yaml declares device but no code imports mainsdk/device; nothing is served |
MAIN-DEVICE-0003 | Checkmainsdk check, before deploy | The device section is invalid: unknown driver or key, a value out of range, authority written as a literal instead of secret:NAME, more than 2 authority keys, an oidc entry without issuer, audience or tenant, or a proof not in the list. The app doesn't start |
MAIN-DEVICE-0004 | Checkmainsdk check, before deploy | driver: taps without @mainsdk/device-taps installed, or an @taps/* import outside that package |
MAIN-DEVICE-0016 | Checkmainsdk check, before deploy | Production runs with no device.authority, or with the development authority key that mainsdk dev generated |
MAIN-DEVICE-0022 | Checkmainsdk check, before deploy | The device.attest section is invalid: require: hardware or environment, formats, roots, builds or min-security-level with driver: local; an unknown format; a root or builds file that doesn't exist; every or grace out of range |
MAIN-DEVICE-0038 | Checkmainsdk check, before deploy | The device.telemetry section is invalid: a stream name that doesn't match the pattern, a size or rate out of range, redact that isn't a list of paths |
MAIN-DEVICE-0039 | Checkmainsdk check, before deploy | device.telemetry.on names a stream that isn't declared, or a declared stream has no handler |
MAIN-DEVICE-0050 | Checkmainsdk check, before deploy | The device.policy section is invalid: stages that don't increase or don't end at 100%, hold outside 5m to 7d, rollback-if outside 1% to 50%, approve above 2, or an unknown tighten direction |
MAIN-DEVICE-0066 | Checkmainsdk check, before deploy | A client.commands.handle handler passes its input to a process or an evaluator (system.run, child_process, eval, new Function, vm, dynamic import()) |
MAIN-DEVICE-0067 | Checkmainsdk check, before deploy | A command type is unsafe to declare: its input schema has an unbounded string field named command, cmd, script, code, shell, exec or args; it uses the mainsdk. prefix; it is destructive with approve: 0; or it is reaction: true while destructive, or write without undo |
MAIN-DEVICE-0071 | Checkmainsdk check, before deploy | The device.commands section is invalid: a value out of range, or ttl over max-ttl |
MAIN-DEVICE-0082 | Checkmainsdk check, before deploy | The device.heartbeat section is invalid: every outside 15s to 1h, late-after outside 2 to 10, silent-after outside 3 to 100 or not above late-after |
MAIN-DEVICE-0092 | Checkmainsdk check, before deploy | The device.taps section is invalid: a did:web without a key-history log, TAPS-CS3, an unknown delegation target, or a status refresh outside 5m to 24h |
MAIN-DEVICE-0005 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0006 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0007 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0008 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0009 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0010 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0011 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0012 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0013 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0014 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0015 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0017 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0018 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0020 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0021 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0023 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0024 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0030 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0031 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0032 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0033 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0034 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0035 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0036 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0037 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0045 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0046 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0047 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0048 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0049 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0051 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0060 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0061 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0062 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0063 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0064 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0065 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0068 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0069 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0070 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0080 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0081 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0083 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0090 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0091 | RuntimeError responses and logs | See the spec |
MAIN-DEVICE-0093 | RuntimeError responses and logs | See the spec |
Features
Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.
| Feature | Phase | Milestone | Spec ticket |
|---|---|---|---|
| Opt-in subpaths and half-declaration checks | Phase 4 | 1.3 | device-identity#opt-in |
| Enrolment by token, approved code and workload OIDC | Phase 4 | 1.3 | device-identity#enrol-token |
| Renewal, rotation and revocation | Phase 4 | 1.3 | device-identity#renew-rotate |
| Device client and key stores | Phase 4 | 1.3 | device-identity#client |
| Simulated devices and the conformance suite | Phase 4 | 1.3 | device-identity#sim |
| Attestation levels, nonces and appraisal policy | Phase 4 | 1.3 | device-attest#policy |
| Signed ordered telemetry with an offline buffer | Phase 4 | 1.3 | device-telemetry#ingest |
| Heartbeat | Phase 4 | 1.3 | device-health#states |
| Policy bundles with staged rollout and rollback | Phase 4 | 1.4 | device-policy#rollout |
| Tighten-only policy overlays as playbook steps | Phase 4 | 1.4 | device-policy#overlays |
| Signed commands, approvals, receipts and undo | Phase 4 | 1.4 | device-commands#deliver |
| TAPS credentials, status lists, evidence and delegation | Phase 4 | 1.4 | device-taps#credentials |
MainSDK Cloud
Everything above is free in your app. Cloud adds these on top, by plan:
| Plan | What it adds |
|---|---|
| Fleet | Fleet enrols MainSDK apps themselves through this plumbing; the in-app interface stays free |
Quick start
schema: 1
device:
driver: local # local | taps (from @mainsdk/device-taps)
path: /mainsdk/device
authority: secret:MAINSDK_DEVICE_KEY
enrol: { proofs: [token, code] }
attest: { require: none }
telemetry: { streams: { app.status: {} }, max-batch: 1MB }
heartbeat: { every: 60s }
policy: { rollout: { stages: ["5%", "25%", "100%"], hold: 1h } }
commands: { ttl: 15m, max-targets: 100 }import { device } from "mainsdk/device"; // not exported from "mainsdk"
import { deviceClient, fileKeyStore } from "mainsdk/device/client";
const { token } = await device.enrolments.create({ group: "lab", uses: 10 });
device.telemetry.on("app.status", async (records) => { console.log(records.length); });
// on the device
const client = deviceClient({ url: "https://app.example.com/mainsdk/device", keys: fileKeyStore("/var/lib/acme") });
await client.enrol({ token });
await client.start();
client.send("app.status", { ok: true });Calls
Enrolment and identity are built (spec device-identity); the other calls follow their own specs (telemetry, policy, commands, health) and are still being built. Server calls are from mainsdk/device; device-side calls are from mainsdk/device/client. Server calls read the tenant from the context.
device.policy and device.commands are not loaded with mainsdk/device, which stays small for apps that only enrol devices. Call mountPolicy(device) from mainsdk/device/policy and mountCommands(device) from mainsdk/device/commands once where the app starts. On the device, mountPolicy(client) and mountCommands(client) from mainsdk/device/policy/client and mainsdk/device/commands/client add client.policy and client.commands. Using one before its mount call throws an error that names the subpath.
| Call | What it does |
|---|---|
device.enrolments.create({ group, uses, expiresIn }) | Makes a one-time enrolment token (prefix mde_) bound to a group, with a number of uses and an expiry. The token is shown once and only its hash is stored. uses defaults to 1 |
device.enrolments.list({ cursor }) | Lists enrolments a page at a time. It never returns the token |
device.enrolments.revoke(id, { reason }) | Revokes an enrolment so its token can't enrol more devices |
device.get(id) | Returns one device: its did, group, labels, state, attestation level, credential expiry and last-seen time |
device.list({ group, state, cursor }) | Lists devices a page at a time, filtered by group or by state (pending, online, late, silent, revoked) |
device.update(id, { name, group, labels }) | Changes a device's name, group or labels |
device.revoke(id, { reason }) | Revokes a device on the next request on every instance. A revoked id is never reactivated; the device enrols again under a new id |
device.handler() | Returns the (req: Request) => Promise<Response> handler for the device routes. The adapters mount it at device.path when device is declared |
device.telemetry.on(stream, handler, { schema }) | Registers the handler that receives a stream's records, in order per device, inside the device's tenant. A batch is acknowledged only after every handler resolves. Storing records is the handler's job. Returns an unsubscribe function |
device.summary.get({ group }) | Returns counts only (pending, online, late, silent, revoked, policy failed, attestation lapsed, clock skewed, commands waiting), optionally for one group |
device.policy.define(schema, { tighten }) | Declares the schema every policy bundle must pass, and the paths an overlay may tighten and in which direction. Call it once at start, before the first publish |
device.policy.publish(content, { target, reason, rollout }) | Publishes a signed policy bundle as the next version for a target (the tenant or one group), rolled out in stages with a hold between them. Content that fails the schema is refused |
device.policy.get(version, { target }) | Returns one policy version with its state and the counts of devices that applied it, failed or are pending |
device.policy.list({ target, cursor }) | Lists policy versions a page at a time |
device.policy.rollback({ target, to, reason }) | Publishes an old version's content again as a new version, so devices' anti-rollback holds |
device.policy.tighten({ target, path, value, expiresIn, reason }) | Adds an overlay that makes one declared path stricter for at most 24 hours. An overlay that would loosen a value, or change a path not declared under tighten, is refused |
device.commands.define(type, { input, risk, approve, undo }) | Registers a command type with its input schema, its risk (read, write or destructive), how many people must approve it and the type that undoes it. Define a type before creating it |
device.commands.create(type, { to, input, reason }) | Creates one signed command per target device (a device, a list or a group) and returns the batch. A command expires, and waits for approval when its type needs it |
device.commands.get(id) | Returns one command with its state, result and approvals |
device.commands.list({ device, state, cursor }) | Lists commands a page at a time, by device or by state |
device.commands.cancel(id, { reason }) | Cancels a command, only before it is delivered |
device.commands.revert(id, { reason }) | Sends the command type's undo command to the same device, with the original input and result. Refused when the type has no undo or the command isn't done |
device.mcp.tools() | Returns device.commands.create as an action for the MCP server that always waits for a person to approve. An agent can ask, never approve |
deviceClient({ url, keys, dir }) | Makes the client for the program that runs on the device (mainsdk/device/client). It has enrol, start, stop, rotate, state, send, flush and heartbeat, and needs no main.yaml. Pass keys or let dir hold the key |
fileKeyStore(dir, { alg }) | Key store for Node, Bun and Deno. Writes the key as a PKCS#8 file with mode 0600, in a directory with mode 0700. alg is Ed25519 or ES256 |
memoryKeyStore({ alg }) | Key store held in memory, for tests |
webCryptoKeyStore({ db }) | Key store for browsers and kiosks. The key is a non-extractable CryptoKey in IndexedDB |
softwareEvidence({ software }) | Evidence provider for the software attestation level, naming the program and its version. Pass it as the client's attest option |
simulateDevices(app, { count }) | From @mainsdk/testing: enrols simulated devices, each a real deviceClient on a memory key store, and drives them on the fake clock |
deviceConformance({ driver }) | From @mainsdk/testing: the suite every device driver passes |
Configuration
device:
driver: local # local | taps (taps from @mainsdk/device-taps)
path: /mainsdk/device # where device.handler() is mounted
audience: acme-fleet # the aud every device envelope carries; default the project id
authority: secret:MAINSDK_DEVICE_KEY # Ed25519 PKCS#8 that signs credentials; a list of 2 rolls it over
max: 10000 # devices per tenant; 1 to 1,000,000
concurrency: 64 # device requests in flight per instance
enrol:
proofs: [token, code] # token | code | oidc | attested
token-ttl: 24h # default and longest expiresIn for a token; 5m to 30d
credential:
ttl: 24h # credential lifetime, renewed by the heartbeat; 1h to 30d
renew-grace: 30d # an expired credential can still be renewed with the device key| Key | What it does |
|---|---|
driver | local runs on did:key with Ed25519 or P-256 and no dependency. taps comes from @mainsdk/device-taps. |
path | Where device.handler() is mounted. The adapters mount it when device is declared. |
authority | The key that signs credentials, policy and commands. Always secret:NAME, never a literal. Two entries roll it over. |
max, concurrency | The device cap per tenant, and the device requests in flight per instance. Over the second, devices get MAIN-DEVICE-0012 with Retry-After. |
enrol.proofs | Which proofs may enrol a device: a one-time token, a code a signed-in person approves, an oidc token, or attested evidence. |
credential | How long a credential lives and how long past expiry the device key can still renew it. |
A revoked device is refused on the next request on every instance. A third party that checks a credential offline learns of a revocation only when the credential expires, so credential.ttl bounds that window.
What you get
Enrolment by one-time token, code, OIDC token or attested evidence. A device key made on the device that never leaves it. A did:key identity, a signed credential with renewal, rotation and revocation, every message signed, fresh and bound to one tenant, and a registry in the tenant's region. The device side is mainsdk/device/client, which needs no main.yaml.
Examples
import { device } from "mainsdk/device";
const { token, uses } = await device.enrolments.create({ group: "lab", uses: 3 });
const page = await device.list({ group: "lab" });Drivers
| Driver | Provider | |
|---|---|---|
local | Local (did:key, Ed25519 or P-256, no dependency) | default |
taps | TAPS credentials, attestation and delegation |
What it always does
- Off until declared in main.yaml and imported; no route, table, timer or cost otherwise, and never re-exported from mainsdk
- Device keys are made on the device and never leave it
- Every device message is signed, fresh and bound to one tenant
- Telemetry is delivered once and in order, with redaction on the device and on the server
- A device keeps its last good policy when the app is unreachable and never accepts an older one
- Commands run only registered handlers, expire, and need people's approval when risky; an agent never approves
- No detection rules, endpoint event schemas or code delivery, now or later (ADR-0076)
Errors and checks
| Code | When |
|---|---|
MAIN-DEVICE-0001 | mainsdk/device is imported but main.yaml has no device section; device calls throw it at runtime (500) |
MAIN-DEVICE-0002 | main.yaml declares device but no code imports mainsdk/device; nothing is served |
MAIN-DEVICE-0003 | The device section is invalid: unknown driver or key, a value out of range, authority written as a literal instead of secret:NAME, more than 2 authority keys, an oidc entry without issuer, audience or tenant, or a proof not in the list. The app doesn't start |
MAIN-DEVICE-0004 | driver: taps without @mainsdk/device-taps installed, or an @taps/* import outside that package |
MAIN-DEVICE-0005 | An envelope was refused: payload not in JCS form, unknown payload type, bad signature, unknown key id, aud not this app (401) |
MAIN-DEVICE-0006 | A request was not fresh: iat more than 10 minutes from server time, or the nonce was seen in the last 20 minutes (401, with serverTime) |
MAIN-DEVICE-0007 | An enrolment proof was refused: the token is unknown, expired, used up or revoked; the code wasn't approved or expired; the OIDC token doesn't match a declared issuer, audience and subject (403). Evidence below a required level is MAIN-DEVICE-0023 |
MAIN-DEVICE-0008 | The device is revoked; every request from it is refused (403, with a signed revoked object) |
MAIN-DEVICE-0009 | The credential expired more than renew-grace ago; the device must enrol again (401) |
MAIN-DEVICE-0010 | A rotation was refused: a signature from the old or new key is missing, prev doesn't match the current credential, or the new key was used before (409) |
MAIN-DEVICE-0011 | The tenant has device.max devices that aren't revoked; enrolment refused (403) |
MAIN-DEVICE-0012 | The device API is busy or its store doesn't answer; try later (503, Retry-After) |
MAIN-DEVICE-0013 | The device belongs to another region; the response names the base to use (421) |
MAIN-DEVICE-0014 | A device.* call was refused: no tenant in context, or no device.manage or device.view permission (403) |
MAIN-DEVICE-0015 | A client call was refused: enrol on an enrolled client without first deleting its key, or a key store that can't be read or written |
MAIN-DEVICE-0016 | Production runs with no device.authority, or with the development authority key that mainsdk dev generated |
MAIN-DEVICE-0017 | Too many enrolment attempts for one token or one address; try later (429, Retry-After) |
MAIN-DEVICE-0018 | The device or enrolment doesn't exist in the caller's tenant; a device of another tenant is reported the same way (404) |
MAIN-DEVICE-0020 | Evidence was refused: format not accepted, binding or nonce wrong or older than 60 seconds, a chain to a root the tenant doesn't accept, failed boot verification, a security level under min-security-level, or a measurement not in builds. The device gets the code only; the server log names the check (403) |
MAIN-DEVICE-0021 | A device missed its re-attestation past grace; its level dropped to none (logged, device.attestation.failed signal) |
MAIN-DEVICE-0022 | The device.attest section is invalid: require: hardware or environment, formats, roots, builds or min-security-level with driver: local; an unknown format; a root or builds file that doesn't exist; every or grace out of range |
MAIN-DEVICE-0023 | An enrolment, a renewal, a policy target or a command needs a higher attestation level than the device holds (403, with need and have) |
MAIN-DEVICE-0024 | The device's TCB is out-of-date past its grace or revoked; renewal refused until it re-attests on a fixed platform (403) |
MAIN-DEVICE-0030 | client.send refused a record: the stream isn't declared in the client or by the server, the record isn't a JSON object, it is over max-record, or it fails the stream's schema on the device |
MAIN-DEVICE-0031 | A batch is over max-batch or holds more than 1,000 records (413) |
MAIN-DEVICE-0032 | A batch is out of order: seq isn't the next one, or prev doesn't match the last accepted batch (409, with expect) |
MAIN-DEVICE-0033 | A record was rejected: unknown stream, over its stream's max-record, or it fails the stream's schema on the server |
MAIN-DEVICE-0034 | A record was rejected because the detectors found a secret, or personal data in a stream with personal: false |
MAIN-DEVICE-0035 | Telemetry is rate limited: the device's rate or the tenant's daily bytes is reached (429, Retry-After) |
MAIN-DEVICE-0036 | A handler threw or took longer than 10 seconds; the batch wasn't acknowledged and will come again (503) |
MAIN-DEVICE-0037 | The client's buffer is full or holds records older than queue.age; the oldest were dropped (logged once an hour with counts per stream) |
MAIN-DEVICE-0038 | The device.telemetry section is invalid: a stream name that doesn't match the pattern, a size or rate out of range, redact that isn't a list of paths |
MAIN-DEVICE-0039 | device.telemetry.on names a stream that isn't declared, or a declared stream has no handler |
MAIN-DEVICE-0045 | A publish was refused: no device.policy.define, content that fails the schema, an unknown group, or minClient that isn't a version (400) |
MAIN-DEVICE-0046 | An overlay was refused: its path isn't under tighten, or the value wouldn't tighten the value in force (400) |
MAIN-DEVICE-0047 | The device refused a bundle: bad signature, wrong aud or tenant, a version not above the one it holds, an overlay that loosens, or a minClient above its own; it kept its last good policy and reported why |
MAIN-DEVICE-0048 | The device's apply handler threw; it kept its last good policy and reported failed with the error code |
MAIN-DEVICE-0049 | A rollout stopped: the failed share passed rollback-if; the previous content was published again as a new version (logged, audited, device.policy.rolledback signal) |
MAIN-DEVICE-0050 | The device.policy section is invalid: stages that don't increase or don't end at 100%, hold outside 5m to 7d, rollback-if outside 1% to 50%, approve above 2, or an unknown tighten direction |
MAIN-DEVICE-0051 | The publish waits for approval; it applies when the people approve asks for have approved (202) |
MAIN-DEVICE-0060 | A create was refused: the type isn't defined, the input fails its schema, the target is empty, or expiresIn is over max-ttl (400) |
MAIN-DEVICE-0061 | The create targets more than max-targets devices without confirm equal to the count (409, with count) |
MAIN-DEVICE-0062 | The command waits for approval (202); an app agent's approval was refused |
MAIN-DEVICE-0063 | The device refused a command: bad signature, wrong aud, tenant or device, expired, an id it already ran, a type it hasn't registered, a risk that differs from its handler's, or fewer approvals than its floor; the receipt says refused |
MAIN-DEVICE-0064 | The command expired before it was delivered or before a receipt came back |
MAIN-DEVICE-0065 | The handler threw, timed out or returned a result that fails its schema; the receipt says failed |
MAIN-DEVICE-0066 | A client.commands.handle handler passes its input to a process or an evaluator (system.run, child_process, eval, new Function, vm, dynamic import()) |
MAIN-DEVICE-0067 | A command type is unsafe to declare: its input schema has an unbounded string field named command, cmd, script, code, shell, exec or args; it uses the mainsdk. prefix; it is destructive with approve: 0; or it is reaction: true while destructive, or write without undo |
MAIN-DEVICE-0068 | A cancel or revert was refused: the command was already delivered, its type has no undo, or it isn't done (409) |
MAIN-DEVICE-0069 | A receipt was refused: bad signature, an unknown command, or a digest that doesn't match the command sent (400) |
MAIN-DEVICE-0070 | The device's command-log head differs from the one the server computes from its receipts (security event device.command_log_mismatch, high) |
MAIN-DEVICE-0071 | The device.commands section is invalid: a value out of range, or ttl over max-ttl |
MAIN-DEVICE-0080 | A heartbeat was refused: its seq isn't above the last one (401) |
MAIN-DEVICE-0081 | The device's clock is more than 5 minutes off the app's; the offset is recorded and corrected for (logged once a day per device) |
MAIN-DEVICE-0082 | The device.heartbeat section is invalid: every outside 15s to 1h, late-after outside 2 to 10, silent-after outside 3 to 100 or not above late-after |
MAIN-DEVICE-0083 | A device went silent (logged at info, device.silent signal) |
MAIN-DEVICE-0090 | The status list couldn't be refreshed; the last list is used until its validUntil, after which third parties treat credentials as unverifiable, while the app's own checks go on (logged once per outage) |
MAIN-DEVICE-0091 | A delegation chain or approval decision on a command or publish is invalid: a broken chain, authority that widens down the chain, a revoked or expired link, or an approval by a key not in the tenant's DID document; the server refuses to send it and a device refuses to run it |
MAIN-DEVICE-0092 | The device.taps section is invalid: a did:web without a key-history log, TAPS-CS3, an unknown delegation target, or a status refresh outside 5m to 24h |
MAIN-DEVICE-0093 | An agent passport was refused: an operator not listed, a passport not countersigned by the tenant, or a heartbeat reporting a model or version the passport doesn't name (TAPS-ATTEST §3: a change needs a new passport) |
See check codes.
Swapping drivers
device:
driver: taps # needs @mainsdk/device-tapsThe taps driver issues the TAPS credential form from the same device record. Nothing else in your code changes.
Testing
simulateDevices(app, { count }) from @mainsdk/testing enrols simulated devices, each a real deviceClient on a memory key store, and drives them on the fake clock. deviceConformance({ driver }) is the suite every driver passes.
Next
Was this page helpful?