dacument 1.2.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,258 +1,260 @@
1
- # Dacument
2
-
3
- Dacument is a schema-driven CRDT document that signs every operation and
4
- enforces role-based ACLs at merge time. Local writes emit signed ops; state
5
- advances only when ops are merged. It gives you a JS object-like API for
6
- register fields and safe CRDT views for all other field types.
7
-
8
- ## Install
9
-
10
- ```sh
11
- npm install dacument
12
- # or
13
- pnpm add dacument
14
- # or
15
- yarn add dacument
16
- ```
17
-
18
- ## Quick start
19
-
20
- ```ts
21
- import { generateNonce } from "bytecodec";
22
- import { generateSignPair } from "zeyra";
23
- import { Dacument } from "dacument";
24
-
25
- const actorId = generateNonce(); // 256-bit base64url id
26
- const actorKeys = await generateSignPair();
27
- await Dacument.setActorInfo({
28
- id: actorId,
29
- privateKeyJwk: actorKeys.signingJwk,
30
- publicKeyJwk: actorKeys.verificationJwk,
31
- });
32
-
33
- const schema = Dacument.schema({
34
- title: Dacument.register({ jsType: "string", regex: /^[a-z ]+$/i }),
35
- body: Dacument.text(),
36
- items: Dacument.array({ jsType: "string" }),
37
- tags: Dacument.set({ jsType: "string" }),
38
- meta: Dacument.record({ jsType: "string" }),
39
- });
40
-
41
- const { docId, snapshot, roleKeys } = await Dacument.create({ schema });
42
-
43
- const doc = await Dacument.load({
44
- schema,
45
- roleKey: roleKeys.owner.privateKey,
46
- snapshot,
47
- });
48
-
49
- doc.title = "Hello world";
50
- doc.body.insertAt(0, "H");
51
- doc.tags.add("draft");
52
- doc.items.push("milk");
53
-
54
- // Wire ops to your transport.
55
- doc.addEventListener("change", (event) => channel.send(event.ops));
56
- channel.onmessage = (ops) => doc.merge(ops);
57
-
58
- doc.addEventListener("merge", ({ actor, target, method, data }) => {
59
- // Update UI from a single merge stream.
60
- });
61
- ```
62
-
63
- `create()` returns `roleKeys` for owner/manager/editor; store them securely and
64
- distribute the highest role key each actor should hold.
65
-
66
- ## Schema and fields
67
-
68
- - `register` fields behave like normal properties: `doc.title = "hi"`.
69
- - Other fields return safe CRDT views: `doc.items.push("x")`.
70
- - Unknown fields or schema bypasses throw.
71
- - UI updates should listen to `merge` events.
72
-
73
- Supported CRDT field types:
74
-
75
- - `register` - last writer wins register.
76
- - `text` - text RGA.
77
- - `array` - array RGA.
78
- - `set` - OR-Set.
79
- - `map` - OR-Map.
80
- - `record` - OR-Record.
81
-
82
- Map keys must be JSON-compatible values (`string`, `number`, `boolean`, `null`,
83
- arrays, or objects). For string-keyed data, prefer `record`. For non-JSON or
84
- identity-based keys, use `CRMap` with a stable `key` function.
85
-
86
- ## Roles and ACL
87
-
88
- Roles are evaluated at the op stamp time (HLC).
89
-
90
- - Owner: full control (including ownership transfer).
91
- - Manager: can grant editor/viewer/revoked roles (cannot change owner).
92
- - Editor: can write non-ACL fields.
93
- - Viewer: read-only.
94
- - Revoked: reads are masked to initial values; writes are rejected.
95
-
96
- Grant roles via `doc.acl` (viewer/revoked have no key):
97
-
98
- ```ts
99
- const bobId = generateNonce();
100
- doc.acl.setRole(bobId, "editor");
101
- doc.acl.setRole("user-viewer", "viewer");
102
- await doc.flush();
103
- ```
104
-
105
- Before any schema/load/create, call `await Dacument.setActorInfo(...)` once per
106
- process. The actor id must be a 256-bit base64url string (e.g.
107
- `bytecodec` library's `generateNonce()`), and the actor key pair must be ES256 (P-256).
108
- Updating actor info requires providing the current keys. On first merge, Dacument
109
- auto-attaches the actor's `publicKeyJwk` to its own ACL entry (if missing) and
110
- pins it for actor-signature verification. To rotate actor keys in-process, call
111
- `Dacument.setActorInfo` again with the new keys plus
112
- `currentPrivateKeyJwk`/`currentPublicKeyJwk`.
113
-
114
- Write ops are role-signed; acks are actor-signed with the per-actor key set via
115
- `setActorInfo`. Load with the highest role key you have; viewers load without a
116
- key. Role keys are generated once at `create()`; role public keys are embedded
117
- in the snapshot and never rotated.
118
-
119
- ## Networking and sync
120
-
121
- Use `change` events to relay signed ops, and `merge` to apply them:
122
-
123
- ```ts
124
- doc.addEventListener("change", (event) => send(event.ops));
125
-
126
- // on remote
127
- await peer.merge(ops);
128
- ```
129
-
130
- Local writes do not update state until merged. If you want a single UI update
131
- path, broadcast ops (even back to yourself) and drive UI from `merge` events.
132
-
133
- `merge` events mirror the confirmed operation parameters (e.g. `insertAt`,
134
- `deleteAt`, `push`, `pop`, `set`, `add`) so UIs can apply minimal updates
135
- without snapshotting.
136
-
137
- To add a new replica, share a snapshot and load it:
138
-
139
- ```ts
140
- const bobKeys = await generateSignPair();
141
- await Dacument.setActorInfo({
142
- id: bobId,
143
- privateKeyJwk: bobKeys.signingJwk,
144
- publicKeyJwk: bobKeys.verificationJwk,
145
- });
146
- const bob = await Dacument.load({
147
- schema,
148
- roleKey: bobKey.privateKey,
149
- snapshot,
150
- });
151
- ```
152
-
153
- Snapshots do not include the schema or schema ids; callers must supply the schema on load.
154
-
155
- ## Events and values
156
-
157
- - `doc.addEventListener("change", handler)` emits ops for network sync
158
- (writer ops are role-signed; acks are actor-signed by non-revoked actors and
159
- verified against ACL-pinned actor public keys).
160
- - `doc.addEventListener("merge", handler)` emits `{ actor, target, method, data }`.
161
- - `doc.addEventListener("error", handler)` emits signing/verification errors.
162
- - `doc.addEventListener("revoked", handler)` fires when the current actor is revoked.
163
- - `doc.addEventListener("reset", handler)` emits `{ oldDocId, newDocId, ts, by, reason }`.
164
- - `doc.selfRevoke()` emits a signed ACL op that revokes the current actor.
165
- - `await doc.accessReset({ reason })` creates a new Dacument with fresh keys and emits a reset op.
166
- - `doc.getResetState()` returns reset metadata (or `null`).
167
- - `await doc.flush()` waits for pending signatures so all local ops are emitted.
168
- - `doc.snapshot()` returns a loadable op log (`{ docId, roleKeys, ops }`).
169
- - `await doc.verifyActorIntegrity(...)` verifies per-actor signatures on demand.
170
- - Revoked actors cannot snapshot; reads are masked to initial values.
171
-
172
- ## Access reset (key compromise response)
173
-
174
- If an owner suspects role key compromise, call `accessReset()` to fork to a new
175
- docId and revoke the old one:
176
-
177
- ```ts
178
- const { newDoc, oldDocOps, newDocSnapshot, roleKeys } =
179
- await doc.accessReset({ reason: "suspected compromise" });
180
- ```
181
-
182
- `accessReset()` materializes the current state into a new Dacument with fresh
183
- role keys, emits a signed `reset` op for the old doc, and returns the new
184
- snapshot + keys. The reset is stored as a CRDT op so all replicas converge. Once
185
- reset, the old doc rejects any ops after the reset stamp and throws on writes:
186
- `Dacument is reset/deprecated. Use newDocId: <id>`. Snapshots and verification
187
- still work so you can archive/inspect history.
188
- If an attacker already has the owner key, they can also reset; this is a
189
- response tool for suspected compromise, not a prevention mechanism.
190
-
191
- ## Actor identity (cold path)
192
-
193
- Every op may include an `actorSig` (detached ES256 signature over the op token).
194
- Merges ignore `actorSig` by default to keep the hot path fast. When you need
195
- attribution or forensic checks, call `verifyActorIntegrity()` with a token,
196
- ops list, or snapshot. It verifies `actorSig` against the actor's `publicKeyJwk`
197
- from the ACL at the op stamp and returns a summary plus failures.
198
-
199
- ## Garbage collection
200
-
201
- Dacument tracks per-actor `ack` ops and compacts tombstones once all non-revoked
202
- actors (including viewers) have acknowledged a given HLC. Acks are emitted
203
- automatically after merges that apply new non-ack ops. Acks are ES256 actor-signed
204
- by non-revoked actors and verified against ACL-pinned actor public keys.
205
- If any non-revoked actor is offline and never acks, tombstones are kept.
206
-
207
- ## Guarantees
208
-
209
- - Schema enforcement is strict; unknown fields are rejected.
210
- - Ops are accepted only if the CRDT patch is valid and the role signature
211
- verifies; acks require a valid actor signature.
212
- - Role checks are applied at the op stamp time (HLC).
213
- - IDs are base64url nonces from `bytecodec` library's `generateNonce()` (32 random bytes).
214
- - Private keys are returned by `create()` and never stored by Dacument.
215
- - Snapshots may include ops that are rejected; invalid ops are ignored on load.
216
-
217
- Eventual consistency is achieved when all signed ops are delivered to all
218
- replicas. Dacument does not provide transport; use `change` events to wire it up.
219
-
220
- ## Possible threats and how to handle
221
-
222
- - Role key compromise: role keys cannot be rotated; use `accessReset` or
223
- snapshot into a new Dacument with fresh keys. Actor keys can be rotated by
224
- providing the current keys.
225
- - Shared role keys: attribution is role-level, not per-user; treat roles as
226
- trust groups and log merge events if you need auditing.
227
- - Insider DoS/flooding: rate-limit ops, cap payload sizes, and monitor merge
228
- errors at the application layer.
229
- - Withholding/delivery delays: eventual consistency depends on ops arriving;
230
- use reliable transport and resync via snapshot when needed.
231
- - No built-in encryption: use TLS or E2E encryption and treat snapshots as
232
- sensitive data.
233
-
234
- ## Compatibility
235
-
236
- - ESM only (`type: module`).
237
- - Requires WebCrypto (`node >= 18` or modern browsers).
238
-
239
- ## Scripts
240
-
1
+ # Dacument
2
+
3
+ Dacument is a schema-driven CRDT document that signs every operation and
4
+ enforces role-based ACLs at merge time. Local writes emit signed ops; state
5
+ advances only when ops are merged. It gives you a JS object-like API for
6
+ register fields and safe CRDT views for all other field types.
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ npm install dacument
12
+ # or
13
+ pnpm add dacument
14
+ # or
15
+ yarn add dacument
16
+ ```
17
+
18
+ ## Quick start
19
+
20
+ ```ts
21
+ import { generateNonce } from "bytecodec";
22
+ import { generateSignPair } from "zeyra";
23
+ import { Dacument } from "dacument";
24
+
25
+ const actorId = generateNonce(); // 256-bit base64url id
26
+ const actorKeys = await generateSignPair();
27
+ await Dacument.setActorInfo({
28
+ id: actorId,
29
+ privateKeyJwk: actorKeys.signingJwk,
30
+ publicKeyJwk: actorKeys.verificationJwk,
31
+ });
32
+
33
+ const schema = Dacument.schema({
34
+ title: Dacument.register({ jsType: "string", regex: /^[a-z ]+$/i }),
35
+ body: Dacument.text(),
36
+ items: Dacument.array({ jsType: "string" }),
37
+ tags: Dacument.set({ jsType: "string" }),
38
+ meta: Dacument.record({ jsType: "string" }),
39
+ });
40
+
41
+ const { docId, snapshot, roleKeys } = await Dacument.create({ schema });
42
+
43
+ const doc = await Dacument.load({
44
+ schema,
45
+ roleKey: roleKeys.owner.privateKey,
46
+ snapshot,
47
+ });
48
+
49
+ doc.title = "Hello world";
50
+ doc.body.insertAt(0, "H");
51
+ doc.tags.add("draft");
52
+ doc.items.push("milk");
53
+
54
+ // Wire ops to your transport.
55
+ doc.addEventListener("delta", (event) => channel.send(event.ops));
56
+ channel.onmessage = (ops) => doc.merge(ops);
57
+
58
+ doc.addEventListener("merge", ({ actor, target, method, data }) => {
59
+ // Update UI from a single merge stream.
60
+ });
61
+ ```
62
+
63
+ `create()` returns `roleKeys` for owner/manager/editor; store them securely and
64
+ distribute the highest role key each actor should hold.
65
+
66
+ ## Schema and fields
67
+
68
+ - `register` fields behave like normal properties: `doc.title = "hi"`.
69
+ - Other fields return safe CRDT views: `doc.items.push("x")`.
70
+ - Unknown fields or schema bypasses throw.
71
+ - UI updates should listen to `merge` events.
72
+
73
+ Supported CRDT field types:
74
+
75
+ - `register` - last writer wins register.
76
+ - `text` - text RGA.
77
+ - `array` - array RGA.
78
+ - `set` - OR-Set.
79
+ - `map` - OR-Map.
80
+ - `record` - OR-Record.
81
+
82
+ Map keys must be JSON-compatible values (`string`, `number`, `boolean`, `null`,
83
+ arrays, or objects). For string-keyed data, prefer `record`. For non-JSON or
84
+ identity-based keys, use `CRMap` with a stable `key` function.
85
+
86
+ ## Roles and ACL
87
+
88
+ Roles are evaluated at the op stamp time (HLC).
89
+
90
+ - Owner: full control (including ownership transfer).
91
+ - Manager: can grant editor/viewer/revoked roles (cannot change owner).
92
+ - Editor: can write non-ACL fields.
93
+ - Viewer: read-only.
94
+ - Revoked: reads are masked to initial values; writes are rejected.
95
+
96
+ Grant roles via `doc.acl` (viewer/revoked have no key):
97
+
98
+ ```ts
99
+ const bobId = generateNonce();
100
+ doc.acl.setRole(bobId, "editor");
101
+ doc.acl.setRole("user-viewer", "viewer");
102
+ await doc.flush();
103
+ ```
104
+
105
+ Before any schema/load/create, call `await Dacument.setActorInfo(...)` once per
106
+ process. The actor id must be a 256-bit base64url string (e.g.
107
+ `bytecodec` library's `generateNonce()`), and the actor key pair must be ES256 (P-256).
108
+ Updating actor info requires providing the current keys. On first merge, Dacument
109
+ auto-attaches the actor's `publicKeyJwk` to its own ACL entry (if missing) and
110
+ pins it for actor-signature verification. To rotate actor keys in-process, call
111
+ `Dacument.setActorInfo` again with the new keys plus
112
+ `currentPrivateKeyJwk`/`currentPublicKeyJwk`.
113
+
114
+ Write ops are role-signed; acks are actor-signed with the per-actor key set via
115
+ `setActorInfo`. Load with the highest role key you have; viewers load without a
116
+ key. Role keys are generated once at `create()`; role public keys are embedded
117
+ in the snapshot and never rotated.
118
+
119
+ ## Networking and sync
120
+
121
+ Use `delta` events to relay signed ops, and `merge` to apply them:
122
+
123
+ ```ts
124
+ doc.addEventListener("delta", (event) => send(event.ops));
125
+
126
+ // on remote
127
+ await peer.merge(ops);
128
+ ```
129
+
130
+ Local writes do not update state until merged. If you want a single UI update
131
+ path, broadcast ops (even back to yourself) and drive UI from `merge` events.
132
+
133
+ `merge` events mirror the confirmed operation parameters (e.g. `insertAt`,
134
+ `deleteAt`, `push`, `pop`, `set`, `add`) so UIs can apply minimal updates
135
+ without snapshotting.
136
+
137
+ To add a new replica, share a snapshot and load it:
138
+
139
+ ```ts
140
+ const bobKeys = await generateSignPair();
141
+ await Dacument.setActorInfo({
142
+ id: bobId,
143
+ privateKeyJwk: bobKeys.signingJwk,
144
+ publicKeyJwk: bobKeys.verificationJwk,
145
+ });
146
+ const bob = await Dacument.load({
147
+ schema,
148
+ roleKey: bobKey.privateKey,
149
+ snapshot,
150
+ });
151
+ ```
152
+
153
+ Snapshots do not include the schema or schema ids; callers must supply the schema on load.
154
+
155
+ ## Events and values
156
+
157
+ - `doc.addEventListener("delta", handler)` emits ops for network sync
158
+ (writer ops are role-signed; acks are actor-signed by non-revoked actors and
159
+ verified against ACL-pinned actor public keys).
160
+ - `doc.addEventListener("merge", handler)` emits `{ actor, target, method, data }`.
161
+ - `doc.addEventListener("error", handler)` emits signing/verification errors.
162
+ - `doc.addEventListener("revoked", handler)` fires when the current actor is revoked.
163
+ - `doc.addEventListener("reset", handler)` emits `{ oldDocId, newDocId, ts, by, reason }`.
164
+ - `doc.selfRevoke()` emits a signed ACL op that revokes the current actor.
165
+ - `await doc.accessReset({ reason })` creates a new Dacument with fresh keys and emits a reset op.
166
+ - `doc.getResetState()` returns reset metadata (or `null`).
167
+ - `await doc.flush()` waits for pending signatures so all local ops are emitted.
168
+ - `doc.snapshot()` returns a loadable op log (`{ docId, roleKeys, ops }`).
169
+ - `await doc.verifyActorIntegrity(...)` verifies per-actor signatures on demand.
170
+ - Revoked actors cannot snapshot; reads are masked to initial values.
171
+
172
+ ## Access reset (key compromise response)
173
+
174
+ If an owner suspects role key compromise, call `accessReset()` to fork to a new
175
+ docId and revoke the old one:
176
+
177
+ ```ts
178
+ const { newDoc, oldDocOps, newDocSnapshot, roleKeys } =
179
+ await doc.accessReset({ reason: "suspected compromise" });
180
+ ```
181
+
182
+ `accessReset()` materializes the current state into a new Dacument with fresh
183
+ role keys, emits a signed `reset` op for the old doc, and returns the new
184
+ snapshot + keys. The reset is stored as a CRDT op so all replicas converge. Once
185
+ reset, the old doc rejects any ops after the reset stamp and throws on writes:
186
+ `Dacument is reset/deprecated. Use newDocId: <id>`. Snapshots and verification
187
+ still work so you can archive/inspect history.
188
+ If an attacker already has the owner key, they can also reset; this is a
189
+ response tool for suspected compromise, not a prevention mechanism.
190
+
191
+ ## Actor identity (cold path)
192
+
193
+ Every op may include an `actorSig` (detached ES256 signature over the op token).
194
+ Merges ignore `actorSig` by default to keep the hot path fast. When you need
195
+ attribution or forensic checks, call `verifyActorIntegrity()` with a token,
196
+ ops list, or snapshot. It verifies `actorSig` against the actor's `publicKeyJwk`
197
+ from the ACL at the op stamp and returns a summary plus failures.
198
+
199
+ ## Garbage collection
200
+
201
+ Dacument tracks per-actor `ack` ops and compacts tombstones once all non-revoked
202
+ actors (including viewers) have acknowledged a given HLC. Acks are emitted
203
+ automatically after merges that apply new non-ack ops. Acks are ES256 actor-signed
204
+ by non-revoked actors and verified against ACL-pinned actor public keys.
205
+ If any non-revoked actor is offline and never acks, tombstones are kept.
206
+
207
+ ## Guarantees
208
+
209
+ - Schema enforcement is strict; unknown fields are rejected.
210
+ - Ops are accepted only if the CRDT patch is valid and the role signature
211
+ verifies; acks require a valid actor signature.
212
+ - Role checks are applied at the op stamp time (HLC).
213
+ - IDs are base64url nonces from `bytecodec` library's `generateNonce()` (32 random bytes).
214
+ - Private keys are returned by `create()` and never stored by Dacument.
215
+ - Snapshots may include ops that are rejected; invalid ops are ignored on load.
216
+
217
+ Eventual consistency is achieved when all signed ops are delivered to all
218
+ replicas. Dacument does not provide transport; use `delta` events to wire it up.
219
+
220
+ ## Possible threats and how to handle
221
+
222
+ - Role key compromise: role keys cannot be rotated; use `accessReset` or
223
+ snapshot into a new Dacument with fresh keys. Actor keys can be rotated by
224
+ providing the current keys.
225
+ - Shared role keys: attribution is role-level, not per-user; treat roles as
226
+ trust groups and log merge events if you need auditing.
227
+ - Insider DoS/flooding: rate-limit ops, cap payload sizes, and monitor merge
228
+ errors at the application layer.
229
+ - Withholding/delivery delays: eventual consistency depends on ops arriving;
230
+ use reliable transport and resync via snapshot when needed.
231
+ - No built-in encryption: use TLS or E2E encryption and treat snapshots as
232
+ sensitive data.
233
+
234
+ ## Compatibility
235
+
236
+ - ESM only (`type: module`).
237
+ - Requires WebCrypto (`node >= 18` or modern browsers).
238
+
239
+ ## Scripts
240
+
241
241
  - `npm test` runs the test suite (build included).
242
+ - `npm run test:types` runs compile-time inference checks.
243
+ - `npm run test:playwright` runs browser/unit/integration/e2e coverage via Playwright.
242
244
  - `npm run bench` runs all CRDT micro-benchmarks (build included).
243
245
  - `npm run sim` runs a worker-thread stress simulation.
244
- - `npm run verify` runs tests, benchmarks, and the simulation in one go.
245
-
246
- ## Benchmarks
247
-
248
- `npm run bench` prints CRDT timings alongside native structure baselines
249
- (Array/Set/Map/string). Use environment variables like `RUNS`, `SIZE`, `READS`,
250
- `WRITES`, and `MERGE_SIZE` to tune scale. Compare the CRDT lines to the native
251
- lines in the output to estimate overhead on your machine. CRDT ops retain
252
- causal metadata and tombstones, so write-heavy paths will be slower than native
253
- structures; the baselines show the relative cost.
254
-
255
- ## Advanced exports
256
-
257
- `CRArray`, `CRMap`, `CRRecord`, `CRRegister`, `CRSet`, and `CRText` are exported
258
- from the package for building custom CRDT workflows.
246
+ - `npm run verify` runs tests (including type + Playwright), benchmarks, and the simulation in one go.
247
+
248
+ ## Benchmarks
249
+
250
+ `npm run bench` prints CRDT timings alongside native structure baselines
251
+ (Array/Set/Map/string). Use environment variables like `RUNS`, `SIZE`, `READS`,
252
+ `WRITES`, and `MERGE_SIZE` to tune scale. Compare the CRDT lines to the native
253
+ lines in the output to estimate overhead on your machine. CRDT ops retain
254
+ causal metadata and tombstones, so write-heavy paths will be slower than native
255
+ structures; the baselines show the relative cost.
256
+
257
+ ## Advanced exports
258
+
259
+ `CRArray`, `CRMap`, `CRRecord`, `CRRegister`, `CRSet`, and `CRText` are exported
260
+ from the package for building custom CRDT workflows.
@@ -746,7 +746,7 @@ export class Dacument {
746
746
  const token = await signToken(this.roleKey, header, payload);
747
747
  const actorSig = await Dacument.signActorToken(token);
748
748
  const oldDocOps = [{ token, actorSig }];
749
- this.emitEvent("change", { type: "change", ops: oldDocOps });
749
+ this.emitEvent("delta", { type: "delta", ops: oldDocOps });
750
750
  await this.merge(oldDocOps);
751
751
  return {
752
752
  newDoc,
@@ -1860,7 +1860,7 @@ export class Dacument {
1860
1860
  ? await Dacument.signActorToken(token, actorSigKey)
1861
1861
  : undefined;
1862
1862
  const op = actorSig ? { token, actorSig } : { token };
1863
- this.emitEvent("change", { type: "change", ops: [op] });
1863
+ this.emitEvent("delta", { type: "delta", ops: [op] });
1864
1864
  })
1865
1865
  .catch((error) => this.emitError(error instanceof Error ? error : new Error(String(error))));
1866
1866
  this.pending.add(promise);
@@ -1879,7 +1879,7 @@ export class Dacument {
1879
1879
  .then(async (token) => {
1880
1880
  const actorSig = await Dacument.signActorToken(token, signingKey);
1881
1881
  const op = { token, actorSig };
1882
- this.emitEvent("change", { type: "change", ops: [op] });
1882
+ this.emitEvent("delta", { type: "delta", ops: [op] });
1883
1883
  })
1884
1884
  .catch((error) => {
1885
1885
  options?.onError?.();
@@ -47,17 +47,20 @@ export type TextSchema = {
47
47
  jsType: "string";
48
48
  initial?: string;
49
49
  };
50
+ type KeyFn<T> = {
51
+ bivarianceHack(value: T): string;
52
+ }["bivarianceHack"];
50
53
  export type ArraySchema<T extends JsTypeName = JsTypeName> = {
51
54
  crdt: "array";
52
55
  jsType: T;
53
56
  initial?: JsTypeValue<T>[];
54
- key?: (value: JsTypeValue<T>) => string;
57
+ key?: KeyFn<JsTypeValue<T>>;
55
58
  };
56
59
  export type SetSchema<T extends JsTypeName = JsTypeName> = {
57
60
  crdt: "set";
58
61
  jsType: T;
59
62
  initial?: JsTypeValue<T>[];
60
- key?: (value: JsTypeValue<T>) => string;
63
+ key?: KeyFn<JsTypeValue<T>>;
61
64
  };
62
65
  export type MapSchema<T extends JsTypeName = JsTypeName> = {
63
66
  crdt: "map";
@@ -98,8 +101,8 @@ export type ResetState = {
98
101
  newDocId: string;
99
102
  reason?: string;
100
103
  };
101
- export type DacumentChangeEvent = {
102
- type: "change";
104
+ export type DacumentDeltaEvent = {
105
+ type: "delta";
103
106
  ops: SignedOp[];
104
107
  };
105
108
  export type DacumentMergeEvent = {
@@ -129,7 +132,7 @@ export type DacumentResetEvent = {
129
132
  reason?: string;
130
133
  };
131
134
  export type DacumentEventMap = {
132
- change: DacumentChangeEvent;
135
+ delta: DacumentDeltaEvent;
133
136
  merge: DacumentMergeEvent;
134
137
  error: DacumentErrorEvent;
135
138
  revoked: DacumentRevokedEvent;
@@ -219,7 +222,9 @@ export type MapView<V> = {
219
222
  export type RecordView<T> = Record<string, T> & {};
220
223
  export type FieldValue<F extends FieldSchema> = F["crdt"] extends "register" ? JsTypeValue<F["jsType"]> : F["crdt"] extends "text" ? TextView : F["crdt"] extends "array" ? ArrayView<JsTypeValue<F["jsType"]>> : F["crdt"] extends "set" ? SetView<JsTypeValue<F["jsType"]>> : F["crdt"] extends "map" ? MapView<JsTypeValue<F["jsType"]>> : F["crdt"] extends "record" ? RecordView<JsTypeValue<F["jsType"]>> : never;
221
224
  export type DocFieldAccess<S extends SchemaDefinition> = {
222
- [K in keyof S]: FieldValue<S[K]>;
225
+ [K in keyof S as S[K]["crdt"] extends "register" ? K : never]: FieldValue<S[K]>;
226
+ } & {
227
+ readonly [K in keyof S as S[K]["crdt"] extends "register" ? never : K]: FieldValue<S[K]>;
223
228
  };
224
229
  export declare function isJsValue(value: unknown): value is JsValue;
225
230
  export declare function isValueOfType(value: unknown, jsType: JsTypeName): boolean;
@@ -256,3 +261,4 @@ export declare function record<T extends JsTypeName>(options: {
256
261
  jsType: T;
257
262
  initial?: Record<string, JsTypeValue<T>>;
258
263
  }): RecordSchema<T>;
264
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dacument",
3
- "version": "1.2.2",
3
+ "version": "2.0.0",
4
4
  "description": "Schema-driven CRDT document with signed ops, role-based ACLs, and per-actor auditability.",
5
5
  "keywords": [
6
6
  "crdt",
@@ -19,9 +19,11 @@
19
19
  "scripts": {
20
20
  "build": "tsc -p tsconfig.json",
21
21
  "test": "npm run build && node --test test/*.test.js",
22
+ "test:types": "tsc -p tsconfig.types.json",
23
+ "test:playwright": "npm run build && playwright test",
22
24
  "bench": "npm run build && node bench/index.js",
23
25
  "sim": "npm run build && node bench/dacument.sim.js",
24
- "verify": "npm run test && npm run bench && npm run sim"
26
+ "verify": "npm run test && npm run test:types && npm run test:playwright && npm run bench && npm run sim"
25
27
  },
26
28
  "main": "dist/index.js",
27
29
  "types": "dist/index.d.ts",
@@ -56,6 +58,7 @@
56
58
  },
57
59
  "devDependencies": {
58
60
  "@types/node": "^25.0.3",
61
+ "playwright": "^1.57.0",
59
62
  "typescript": "^5.9.3"
60
63
  }
61
64
  }