
Security News
Feross on TBPN: How North Korea Hijacked Axios
Socket CEO Feross Aboukhadijeh breaks down how North Korea hijacked Axios and what it means for the future of software supply chain security.
Schema-driven CRDT document with signed ops, role-based ACLs, and per-actor auditability.
Dacument is a schema-driven CRDT document that signs every operation and enforces role-based ACLs at merge time. Local writes emit signed ops; state advances only when ops are merged. It gives you a JS object-like API for register fields and safe CRDT views for all other field types.
npm install dacument
# or
pnpm add dacument
# or
yarn add dacument
import { generateNonce } from "bytecodec";
import { generateSignPair } from "zeyra";
import { Dacument } from "dacument";
const actorId = generateNonce(); // 256-bit base64url id
const actorKeys = await generateSignPair();
await Dacument.setActorInfo({
id: actorId,
privateKeyJwk: actorKeys.signingJwk,
publicKeyJwk: actorKeys.verificationJwk,
});
const schema = Dacument.schema({
title: Dacument.register({ jsType: "string", regex: /^[a-z ]+$/i }),
body: Dacument.text(),
items: Dacument.array({ jsType: "string" }),
tags: Dacument.set({ jsType: "string" }),
meta: Dacument.record({ jsType: "string" }),
});
const { docId, snapshot, roleKeys } = await Dacument.create({ schema });
const doc = await Dacument.load({
schema,
roleKey: roleKeys.owner.privateKey,
snapshot,
});
doc.title = "Hello world";
doc.body.insertAt(0, "H");
doc.tags.add("draft");
doc.items.push("milk");
// Wire ops to your transport.
doc.addEventListener("delta", (event) => channel.send(event.ops));
channel.onmessage = (ops) => doc.merge(ops);
doc.addEventListener("merge", ({ actor, target, method, data }) => {
// Update UI from a single merge stream.
});
create() returns roleKeys for owner/manager/editor; store them securely and
distribute the highest role key each actor should hold.
register fields behave like normal properties: doc.title = "hi".doc.items.push("x").merge events.Supported CRDT field types:
register - last writer wins register.text - text RGA.array - array RGA.set - OR-Set.map - OR-Map.record - OR-Record.Map keys must be JSON-compatible values (string, number, boolean, null,
arrays, or objects). For string-keyed data, prefer record. For non-JSON or
identity-based keys, use CRMap with a stable key function.
Roles are evaluated at the op stamp time (HLC).
Grant roles via doc.acl (viewer/revoked have no key):
const bobId = generateNonce();
doc.acl.setRole(bobId, "editor");
doc.acl.setRole("user-viewer", "viewer");
await doc.flush();
Before any schema/load/create, call await Dacument.setActorInfo(...) once per
process. The actor id must be a 256-bit base64url string (e.g.
bytecodec library's generateNonce()), and the actor key pair must be ES256 (P-256).
Updating actor info requires providing the current keys. On first merge, Dacument
auto-attaches the actor's publicKeyJwk to its own ACL entry (if missing) and
pins it for actor-signature verification. To rotate actor keys in-process, call
Dacument.setActorInfo again with the new keys plus
currentPrivateKeyJwk/currentPublicKeyJwk.
Write ops are role-signed; acks are actor-signed with the per-actor key set via
setActorInfo. Load with the highest role key you have; viewers load without a
key. Role keys are generated once at create(); role public keys are embedded
in the snapshot and never rotated.
Use delta events to relay signed ops, and merge to apply them:
doc.addEventListener("delta", (event) => send(event.ops));
// on remote
await peer.merge(ops);
Local writes do not update state until merged. If you want a single UI update
path, broadcast ops (even back to yourself) and drive UI from merge events.
merge events mirror the confirmed operation parameters (e.g. insertAt,
deleteAt, push, pop, set, add) so UIs can apply minimal updates
without snapshotting.
To add a new replica, share a snapshot and load it:
const bobKeys = await generateSignPair();
await Dacument.setActorInfo({
id: bobId,
privateKeyJwk: bobKeys.signingJwk,
publicKeyJwk: bobKeys.verificationJwk,
});
const bob = await Dacument.load({
schema,
roleKey: bobKey.privateKey,
snapshot,
});
Snapshots do not include the schema or schema ids; callers must supply the schema on load.
doc.addEventListener("delta", handler) emits ops for network sync
(writer ops are role-signed; acks are actor-signed by non-revoked actors and
verified against ACL-pinned actor public keys).doc.addEventListener("merge", handler) emits { actor, target, method, data }.doc.addEventListener("error", handler) emits signing/verification errors.doc.addEventListener("revoked", handler) fires when the current actor is revoked.doc.addEventListener("reset", handler) emits { oldDocId, newDocId, ts, by, reason }.doc.selfRevoke() emits a signed ACL op that revokes the current actor.await doc.accessReset({ reason }) creates a new Dacument with fresh keys and emits a reset op.doc.getResetState() returns reset metadata (or null).await doc.flush() waits for pending signatures so all local ops are emitted.doc.snapshot() returns a loadable op log ({ docId, roleKeys, ops }).await doc.verifyActorIntegrity(...) verifies per-actor signatures on demand.If an owner suspects role key compromise, call accessReset() to fork to a new
docId and revoke the old one:
const { newDoc, oldDocOps, newDocSnapshot, roleKeys } =
await doc.accessReset({ reason: "suspected compromise" });
accessReset() materializes the current state into a new Dacument with fresh
role keys, emits a signed reset op for the old doc, and returns the new
snapshot + keys. The reset is stored as a CRDT op so all replicas converge. Once
reset, the old doc rejects any ops after the reset stamp and throws on writes:
Dacument is reset/deprecated. Use newDocId: <id>. Snapshots and verification
still work so you can archive/inspect history.
If an attacker already has the owner key, they can also reset; this is a
response tool for suspected compromise, not a prevention mechanism.
Every op may include an actorSig (detached ES256 signature over the op token).
Merges ignore actorSig by default to keep the hot path fast. When you need
attribution or forensic checks, call verifyActorIntegrity() with a token,
ops list, or snapshot. It verifies actorSig against the actor's publicKeyJwk
from the ACL at the op stamp and returns a summary plus failures.
Dacument tracks per-actor ack ops and compacts tombstones once all non-revoked
actors (including viewers) have acknowledged a given HLC. Acks are emitted
automatically after merges that apply new non-ack ops. Acks are ES256 actor-signed
by non-revoked actors and verified against ACL-pinned actor public keys.
If any non-revoked actor is offline and never acks, tombstones are kept.
bytecodec library's generateNonce() (32 random bytes).create() and never stored by Dacument.Eventual consistency is achieved when all signed ops are delivered to all
replicas. Dacument does not provide transport; use delta events to wire it up.
accessReset or
snapshot into a new Dacument with fresh keys. Actor keys can be rotated by
providing the current keys.type: module).node >= 18 or modern browsers).npm test runs the test suite (build included).npm run test:types runs compile-time inference checks.npm run test:playwright runs browser/unit/integration/e2e coverage via Playwright.npm run bench runs all CRDT micro-benchmarks (build included).npm run sim runs a worker-thread stress simulation.npm run verify runs tests (including type + Playwright), benchmarks, and the simulation in one go.npm run bench prints CRDT timings alongside native structure baselines
(Array/Set/Map/string). Use environment variables like RUNS, SIZE, READS,
WRITES, and MERGE_SIZE to tune scale. Compare the CRDT lines to the native
lines in the output to estimate overhead on your machine. CRDT ops retain
causal metadata and tombstones, so write-heavy paths will be slower than native
structures; the baselines show the relative cost.
CRArray, CRMap, CRRecord, CRRegister, CRSet, and CRText are exported
from the package for building custom CRDT workflows.
FAQs
Schema-driven CRDT document with signed ops, role-based ACLs, and per-actor auditability.
We found that dacument demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Socket CEO Feross Aboukhadijeh breaks down how North Korea hijacked Axios and what it means for the future of software supply chain security.

Security News
OpenSSF has issued a high-severity advisory warning open source developers of an active Slack-based campaign using impersonation to deliver malware.

Research
/Security News
Malicious packages published to npm, PyPI, Go Modules, crates.io, and Packagist impersonate developer tooling to fetch staged malware, steal credentials and wallets, and enable remote access.