MainSDK
Search⌘K
Interfaces

Platform, OS and network

Connected devices

device

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.

Building for 1.3ships as a beta APIPhase 4 · FleetSpec device-identityFree in your app, Apache-2.0

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.
Stability levels
Updated 2026-10-28

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: 50

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.

main.yamlLocal (did:key, Ed25519 or P-256, no dependency) (default)
device:
  driver: local

Change this one line to swap providers; your calls stay the same.Built-in and local drivers integration

Codes

CodeWhere it appearsMeaning
MAIN-DEVICE-0001Checkmainsdk check, before deploymainsdk/device is imported but main.yaml has no device section; device calls throw it at runtime (500)
MAIN-DEVICE-0002Checkmainsdk check, before deploymain.yaml declares device but no code imports mainsdk/device; nothing is served
MAIN-DEVICE-0003Checkmainsdk check, before deployThe 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-0004Checkmainsdk check, before deploydriver: taps without @mainsdk/device-taps installed, or an @taps/* import outside that package
MAIN-DEVICE-0016Checkmainsdk check, before deployProduction runs with no device.authority, or with the development authority key that mainsdk dev generated
MAIN-DEVICE-0022Checkmainsdk check, before deployThe 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-0038Checkmainsdk check, before deployThe 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-0039Checkmainsdk check, before deploydevice.telemetry.on names a stream that isn't declared, or a declared stream has no handler
MAIN-DEVICE-0050Checkmainsdk check, before deployThe 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-0066Checkmainsdk check, before deployA 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-0067Checkmainsdk check, before deployA 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-0071Checkmainsdk check, before deployThe device.commands section is invalid: a value out of range, or ttl over max-ttl
MAIN-DEVICE-0082Checkmainsdk check, before deployThe 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-0092Checkmainsdk check, before deployThe 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-0005RuntimeError responses and logsSee the spec
MAIN-DEVICE-0006RuntimeError responses and logsSee the spec
MAIN-DEVICE-0007RuntimeError responses and logsSee the spec
MAIN-DEVICE-0008RuntimeError responses and logsSee the spec
MAIN-DEVICE-0009RuntimeError responses and logsSee the spec
MAIN-DEVICE-0010RuntimeError responses and logsSee the spec
MAIN-DEVICE-0011RuntimeError responses and logsSee the spec
MAIN-DEVICE-0012RuntimeError responses and logsSee the spec
MAIN-DEVICE-0013RuntimeError responses and logsSee the spec
MAIN-DEVICE-0014RuntimeError responses and logsSee the spec
MAIN-DEVICE-0015RuntimeError responses and logsSee the spec
MAIN-DEVICE-0017RuntimeError responses and logsSee the spec
MAIN-DEVICE-0018RuntimeError responses and logsSee the spec
MAIN-DEVICE-0020RuntimeError responses and logsSee the spec
MAIN-DEVICE-0021RuntimeError responses and logsSee the spec
MAIN-DEVICE-0023RuntimeError responses and logsSee the spec
MAIN-DEVICE-0024RuntimeError responses and logsSee the spec
MAIN-DEVICE-0030RuntimeError responses and logsSee the spec
MAIN-DEVICE-0031RuntimeError responses and logsSee the spec
MAIN-DEVICE-0032RuntimeError responses and logsSee the spec
MAIN-DEVICE-0033RuntimeError responses and logsSee the spec
MAIN-DEVICE-0034RuntimeError responses and logsSee the spec
MAIN-DEVICE-0035RuntimeError responses and logsSee the spec
MAIN-DEVICE-0036RuntimeError responses and logsSee the spec
MAIN-DEVICE-0037RuntimeError responses and logsSee the spec
MAIN-DEVICE-0045RuntimeError responses and logsSee the spec
MAIN-DEVICE-0046RuntimeError responses and logsSee the spec
MAIN-DEVICE-0047RuntimeError responses and logsSee the spec
MAIN-DEVICE-0048RuntimeError responses and logsSee the spec
MAIN-DEVICE-0049RuntimeError responses and logsSee the spec
MAIN-DEVICE-0051RuntimeError responses and logsSee the spec
MAIN-DEVICE-0060RuntimeError responses and logsSee the spec
MAIN-DEVICE-0061RuntimeError responses and logsSee the spec
MAIN-DEVICE-0062RuntimeError responses and logsSee the spec
MAIN-DEVICE-0063RuntimeError responses and logsSee the spec
MAIN-DEVICE-0064RuntimeError responses and logsSee the spec
MAIN-DEVICE-0065RuntimeError responses and logsSee the spec
MAIN-DEVICE-0068RuntimeError responses and logsSee the spec
MAIN-DEVICE-0069RuntimeError responses and logsSee the spec
MAIN-DEVICE-0070RuntimeError responses and logsSee the spec
MAIN-DEVICE-0080RuntimeError responses and logsSee the spec
MAIN-DEVICE-0081RuntimeError responses and logsSee the spec
MAIN-DEVICE-0083RuntimeError responses and logsSee the spec
MAIN-DEVICE-0090RuntimeError responses and logsSee the spec
MAIN-DEVICE-0091RuntimeError responses and logsSee the spec
MAIN-DEVICE-0093RuntimeError responses and logsSee the spec

Features

Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.

FeaturePhaseMilestoneSpec ticket
Opt-in subpaths and half-declaration checksPhase 41.3device-identity#opt-in
Enrolment by token, approved code and workload OIDCPhase 41.3device-identity#enrol-token
Renewal, rotation and revocationPhase 41.3device-identity#renew-rotate
Device client and key storesPhase 41.3device-identity#client
Simulated devices and the conformance suitePhase 41.3device-identity#sim
Attestation levels, nonces and appraisal policyPhase 41.3device-attest#policy
Signed ordered telemetry with an offline bufferPhase 41.3device-telemetry#ingest
HeartbeatPhase 41.3device-health#states
Policy bundles with staged rollout and rollbackPhase 41.4device-policy#rollout
Tighten-only policy overlays as playbook stepsPhase 41.4device-policy#overlays
Signed commands, approvals, receipts and undoPhase 41.4device-commands#deliver
TAPS credentials, status lists, evidence and delegationPhase 41.4device-taps#credentials

MainSDK Cloud

Everything above is free in your app. Cloud adds these on top, by plan:

PlanWhat it adds
FleetFleet enrols MainSDK apps themselves through this plumbing; the in-app interface stays free

Quick start

main.yaml
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 }
app/example.ts
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.

CallWhat 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

main.yaml
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
KeyWhat it does
driverlocal runs on did:key with Ed25519 or P-256 and no dependency. taps comes from @mainsdk/device-taps.
pathWhere device.handler() is mounted. The adapters mount it when device is declared.
authorityThe key that signs credentials, policy and commands. Always secret:NAME, never a literal. Two entries roll it over.
max, concurrencyThe device cap per tenant, and the device requests in flight per instance. Over the second, devices get MAIN-DEVICE-0012 with Retry-After.
enrol.proofsWhich proofs may enrol a device: a one-time token, a code a signed-in person approves, an oidc token, or attested evidence.
credentialHow 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

app/enrol.ts
import { device } from "mainsdk/device";

const { token, uses } = await device.enrolments.create({ group: "lab", uses: 3 });
const page = await device.list({ group: "lab" });

Drivers

DriverProvider
localLocal (did:key, Ed25519 or P-256, no dependency)default
tapsTAPS 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

CodeWhen
MAIN-DEVICE-0001mainsdk/device is imported but main.yaml has no device section; device calls throw it at runtime (500)
MAIN-DEVICE-0002main.yaml declares device but no code imports mainsdk/device; nothing is served
MAIN-DEVICE-0003The 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-0004driver: taps without @mainsdk/device-taps installed, or an @taps/* import outside that package
MAIN-DEVICE-0005An envelope was refused: payload not in JCS form, unknown payload type, bad signature, unknown key id, aud not this app (401)
MAIN-DEVICE-0006A 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-0007An 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-0008The device is revoked; every request from it is refused (403, with a signed revoked object)
MAIN-DEVICE-0009The credential expired more than renew-grace ago; the device must enrol again (401)
MAIN-DEVICE-0010A 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-0011The tenant has device.max devices that aren't revoked; enrolment refused (403)
MAIN-DEVICE-0012The device API is busy or its store doesn't answer; try later (503, Retry-After)
MAIN-DEVICE-0013The device belongs to another region; the response names the base to use (421)
MAIN-DEVICE-0014A device.* call was refused: no tenant in context, or no device.manage or device.view permission (403)
MAIN-DEVICE-0015A 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-0016Production runs with no device.authority, or with the development authority key that mainsdk dev generated
MAIN-DEVICE-0017Too many enrolment attempts for one token or one address; try later (429, Retry-After)
MAIN-DEVICE-0018The device or enrolment doesn't exist in the caller's tenant; a device of another tenant is reported the same way (404)
MAIN-DEVICE-0020Evidence 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-0021A device missed its re-attestation past grace; its level dropped to none (logged, device.attestation.failed signal)
MAIN-DEVICE-0022The 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-0023An 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-0024The 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-0030client.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-0031A batch is over max-batch or holds more than 1,000 records (413)
MAIN-DEVICE-0032A 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-0033A record was rejected: unknown stream, over its stream's max-record, or it fails the stream's schema on the server
MAIN-DEVICE-0034A record was rejected because the detectors found a secret, or personal data in a stream with personal: false
MAIN-DEVICE-0035Telemetry is rate limited: the device's rate or the tenant's daily bytes is reached (429, Retry-After)
MAIN-DEVICE-0036A handler threw or took longer than 10 seconds; the batch wasn't acknowledged and will come again (503)
MAIN-DEVICE-0037The 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-0038The 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-0039device.telemetry.on names a stream that isn't declared, or a declared stream has no handler
MAIN-DEVICE-0045A 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-0046An overlay was refused: its path isn't under tighten, or the value wouldn't tighten the value in force (400)
MAIN-DEVICE-0047The 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-0048The device's apply handler threw; it kept its last good policy and reported failed with the error code
MAIN-DEVICE-0049A 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-0050The 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-0051The publish waits for approval; it applies when the people approve asks for have approved (202)
MAIN-DEVICE-0060A 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-0061The create targets more than max-targets devices without confirm equal to the count (409, with count)
MAIN-DEVICE-0062The command waits for approval (202); an app agent's approval was refused
MAIN-DEVICE-0063The 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-0064The command expired before it was delivered or before a receipt came back
MAIN-DEVICE-0065The handler threw, timed out or returned a result that fails its schema; the receipt says failed
MAIN-DEVICE-0066A 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-0067A 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-0068A cancel or revert was refused: the command was already delivered, its type has no undo, or it isn't done (409)
MAIN-DEVICE-0069A receipt was refused: bad signature, an unknown command, or a digest that doesn't match the command sent (400)
MAIN-DEVICE-0070The device's command-log head differs from the one the server computes from its receipts (security event device.command_log_mismatch, high)
MAIN-DEVICE-0071The device.commands section is invalid: a value out of range, or ttl over max-ttl
MAIN-DEVICE-0080A heartbeat was refused: its seq isn't above the last one (401)
MAIN-DEVICE-0081The 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-0082The 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-0083A device went silent (logged at info, device.silent signal)
MAIN-DEVICE-0090The 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-0091A 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-0092The 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-0093An 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

main.yaml
device:
  driver: taps                        # needs @mainsdk/device-taps

The 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?

↑↓ moveEnter openEsc close

Not decided yet