Skip to content

Changelog

Notable changes to skm. Dates are the day the work landed; versions begin at 0.1.0, and until 1.0.0 a minor bump may change the wire.

  • Signed records. Every device carries an ECDSA P-256 signing key alongside its wrapping key, and signs each record it writes. Receivers verify against the graph’s roster, check the claimed author against the signing device’s member, and reject anything a revoked device claims to have written after its revocation. by is no longer a claim.
  • Partial sync. Opt-in kind and shard metadata on pushed records, and filtered pulls with a cursor per subscription. Off by default: the metadata is readable by the relay forever, and that is a decision an app makes deliberately or not at all.
  • Per-device key custody. Keys are wrapped to devices, not accounts. Linking a device is a ceremony (register a public key, be granted the keyring) instead of copying a private key between machines, and one device can be revoked without touching the others.
  • Stores: OPFS-SQLite for browsers, IndexedDB as the universal fallback, native SQLite for Node (@skm/client/node), and one conformance suite that every implementation passes.
  • Relay hardening: quotas, per-member rate limits, device/member binding checks, append-only schema migrations, a pluggable BlobStore (SQLite or filesystem), and a typed event stream for metrics and audit.
  • Engine: watch() for reactive queries, flush() for “everything is really saved”, typed reads through the kind registry, retries with jittered backoff, and relay rejections surfaced rather than silently dropped.
  • Clock repair: a device whose clock is wrong adopts the relay’s, once, instead of writing records nothing will ever accept.
  • pushDebounceMs: how long a write waits for its neighbours before the queue goes. Default 250ms, as before.
  • Pushes no longer scan the graph. Quotas were enforced with COUNT(*) and SUM(size) on every write, so a graph of 100k records pushed about five times slower than an empty one. Usage is counted incrementally now, and npm run bench fails if a scan comes back.
  • A push larger than 1MB stalled the queue forever. The route inherited Fastify’s default body limit, and the resulting 413 is not retryable.
  • Two devices creating or rotating a graph at once kept different keys. The relay answers the second one 200, not 409; establishing an epoch is now confirmed by reading back what landed.
  • Equal revisions diverged permanently. A tie is resolved toward the relay’s copy, which is the one everybody else has.
  • A wrap a device could not open locked it out silently. It is discarded and re-granted.
  • A failed roster fetch dropped records as if their authors were unknown.
  • An edit could be stamped below the revision it replaced, whenever the record being edited came from a device whose clock ran ahead. The relay dropped the push as a last-write-wins loser, the local store kept the edit, and the pull cursor was already past the relay’s copy — so the edit lived on one device and nowhere else, permanently, with no error anywhere. A write now reads what it replaces and clears its revision. Found by npm run soak.
  • A device needed two syncs to read records written after a rotation. The retry compared a keyring to itself, because refreshing it mutates the same object.
  • A rotation took up to a minute to reach the other devices. They found out by meeting a record they could not open, or when the periodic epoch check came round — and until then kept sealing under the key the rotation was performed to take away. Push and pull now report the graph’s epoch (epoch on both answers, optional on the wire), and a device that hears a higher one fetches keys before its next write.
  • npm run mutate breaks each guarded line in turn — accept a bad signature, drop the record id from the AAD, let a rotation strand a device, resolve a revision tie the other way — and fails if no test goes red. The first run left four mutations alive, and each was a real gap in the suite.
  • npm run soak drives hundreds of randomized convergence scenarios across rotations, deletions, revision ties, five devices and a device linked halfway through. Failing seeds are promoted into the commit suite.
  • The recovery bundle is sealed under a passphrase, and carries graph keys only — never the device keypair. Two machines signing as one device would make revocation meaningless.
  • Personal-graph record ids are derived tags, so the relay cannot join what you pin to what exists.
  • Device public keys are set-once; rebinding a device id is refused.

The substrate lifted out of xdash: crypto boundary, wire protocol, device engine, and the blind relay, with membership and authentication turned into seams (Directory, authenticate) so the platform does not want to own anybody’s users.