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.
[Unreleased]
Section titled “[Unreleased]”- 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.
byis no longer a claim. - Partial sync. Opt-in
kindandshardmetadata 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(*)andSUM(size)on every write, so a graph of 100k records pushed about five times slower than an empty one. Usage is counted incrementally now, andnpm run benchfails 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
(
epochon both answers, optional on the wire), and a device that hears a higher one fetches keys before its next write.
Testing
Section titled “Testing”npm run mutatebreaks 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 soakdrives 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.
Security
Section titled “Security”- 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.
[0.1.0] — extracted
Section titled “[0.1.0] — extracted”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.