dacument 1.0.1 → 1.2.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
@@ -19,10 +19,16 @@ yarn add dacument
19
19
 
20
20
  ```ts
21
21
  import { generateNonce } from "bytecodec";
22
+ import { generateSignPair } from "zeyra";
22
23
  import { Dacument } from "dacument";
23
24
 
24
25
  const actorId = generateNonce(); // 256-bit base64url id
25
- Dacument.setActorId(actorId);
26
+ const actorKeys = await generateSignPair();
27
+ await Dacument.setActorInfo({
28
+ id: actorId,
29
+ privateKeyJwk: actorKeys.signingJwk,
30
+ publicKeyJwk: actorKeys.verificationJwk,
31
+ });
26
32
 
27
33
  const schema = Dacument.schema({
28
34
  title: Dacument.register({ jsType: "string", regex: /^[a-z ]+$/i }),
@@ -93,9 +99,11 @@ doc.acl.setRole("user-viewer", "viewer");
93
99
  await doc.flush();
94
100
  ```
95
101
 
96
- Before any schema/load/create, call `Dacument.setActorId()` once per process.
97
- The actor id must be a 256-bit base64url string (e.g. `bytecodec.generateNonce()`).
98
- Subsequent calls are ignored.
102
+ Before any schema/load/create, call `await Dacument.setActorInfo(...)` once per
103
+ process. The actor id must be a 256-bit base64url string (e.g.
104
+ `bytecodec` libarys `generateNonce()`), and the actor key pair must be ES256 (P-256).
105
+ Subsequent calls are ignored. On first merge, Dacument auto-attaches the
106
+ actor's `publicKeyJwk` to its own ACL entry (if missing).
99
107
 
100
108
  Each actor signs with the role key they were given (owner/manager/editor). Load
101
109
  with the highest role key you have; viewers load without a key.
@@ -123,7 +131,12 @@ without snapshotting.
123
131
  To add a new replica, share a snapshot and load it:
124
132
 
125
133
  ```ts
126
- Dacument.setActorId(bobId);
134
+ const bobKeys = await generateSignPair();
135
+ await Dacument.setActorInfo({
136
+ id: bobId,
137
+ privateKeyJwk: bobKeys.signingJwk,
138
+ publicKeyJwk: bobKeys.verificationJwk,
139
+ });
127
140
  const bob = await Dacument.load({
128
141
  schema,
129
142
  roleKey: bobKey.privateKey,
@@ -139,10 +152,42 @@ Snapshots do not include schema or schema ids; callers must supply the schema on
139
152
  - `doc.addEventListener("merge", handler)` emits `{ actor, target, method, data }`.
140
153
  - `doc.addEventListener("error", handler)` emits signing/verification errors.
141
154
  - `doc.addEventListener("revoked", handler)` fires when the current actor is revoked.
155
+ - `doc.addEventListener("reset", handler)` emits `{ oldDocId, newDocId, ts, by, reason }`.
156
+ - `doc.selfRevoke()` emits a signed ACL op that revokes the current actor.
157
+ - `await doc.accessReset({ reason })` creates a new dacument with fresh keys and emits a reset op.
158
+ - `doc.getResetState()` returns reset metadata (or `null`).
142
159
  - `await doc.flush()` waits for pending signatures so all local ops are emitted.
143
160
  - `doc.snapshot()` returns a loadable op log (`{ docId, roleKeys, ops }`).
161
+ - `await doc.verifyActorIntegrity(...)` verifies per-actor signatures on demand.
144
162
  - Revoked actors cannot snapshot; reads are masked to initial values.
145
163
 
164
+ ## Access reset (key compromise response)
165
+
166
+ If an owner suspects role key compromise, call `accessReset()` to fork to a new
167
+ doc id and revoke the old one:
168
+
169
+ ```ts
170
+ const { newDoc, oldDocOps, newDocSnapshot, roleKeys } =
171
+ await doc.accessReset({ reason: "suspected compromise" });
172
+ ```
173
+
174
+ `accessReset()` materializes the current state into a new dacument with fresh
175
+ role keys, emits a signed `reset` op for the old doc, and returns the new
176
+ snapshot + keys. The reset is stored as a CRDT op so all replicas converge. Once
177
+ reset, the old doc rejects any ops after the reset stamp and throws on writes:
178
+ `Dacument is reset/deprecated. Use newDocId: <id>`. Snapshots and verification
179
+ still work so you can archive/inspect history.
180
+ If an attacker already has the owner key, they can also reset; this is a
181
+ response tool for suspected compromise, not a prevention mechanism.
182
+
183
+ ## Actor identity (cold path)
184
+
185
+ Every op may include an `actorSig` (detached ES256 signature over the op token).
186
+ Merges ignore `actorSig` by default to keep the hot path fast. When you need
187
+ attribution or forensic checks, call `verifyActorIntegrity()` with a token,
188
+ ops list, or snapshot. It verifies `actorSig` against the actor's `publicKeyJwk`
189
+ from the ACL at the op stamp and returns a summary plus failures.
190
+
146
191
  ## Garbage collection
147
192
 
148
193
  Dacument tracks per-actor `ack` ops and compacts tombstones once all non-revoked
@@ -11,6 +11,7 @@ export declare class AclLog {
11
11
  roleAt(actorId: string, stamp: AclAssignment["stamp"]): Role;
12
12
  currentRole(actorId: string): Role;
13
13
  currentEntry(actorId: string): AclAssignment | null;
14
+ publicKeyAt(actorId: string, stamp: AclAssignment["stamp"]): JsonWebKey | null;
14
15
  knownActors(): string[];
15
16
  private insert;
16
17
  }
@@ -55,6 +55,19 @@ export class AclLog {
55
55
  currentEntry(actorId) {
56
56
  return this.currentByActor.get(actorId) ?? null;
57
57
  }
58
+ publicKeyAt(actorId, stamp) {
59
+ const list = this.nodesByActor.get(actorId);
60
+ if (!list || list.length === 0)
61
+ return null;
62
+ for (let index = list.length - 1; index >= 0; index--) {
63
+ const entry = list[index];
64
+ if (compareHLC(entry.stamp, stamp) > 0)
65
+ continue;
66
+ if (entry.publicKeyJwk)
67
+ return entry.publicKeyJwk;
68
+ }
69
+ return null;
70
+ }
58
71
  knownActors() {
59
72
  return [...this.currentByActor.keys()];
60
73
  }
@@ -1,9 +1,21 @@
1
- import { type AclAssignment, type DacumentEventMap, type DocFieldAccess, type DocSnapshot, type RoleKeys, type RolePublicKeys, type SchemaDefinition, type SchemaId, type SignedOp, type Role, array, map, record, register, set, text } from "./types.js";
1
+ import { type AclAssignment, type ActorInfo, type DacumentEventMap, type DocFieldAccess, type DocSnapshot, type RoleKeys, type RolePublicKeys, type SchemaDefinition, type SchemaId, type SignedOp, type VerificationResult, type VerifyActorIntegrityOptions, type Role, array, map, record, register, set, text } from "./types.js";
2
+ type ResetStateInfo = {
3
+ ts: AclAssignment["stamp"];
4
+ by: string;
5
+ newDocId: string;
6
+ reason?: string;
7
+ };
2
8
  export declare class Dacument<S extends SchemaDefinition> {
3
- private static actorId?;
4
- static setActorId(actorId: string): void;
5
- private static requireActorId;
9
+ private static actorInfo?;
10
+ private static actorSigner?;
11
+ static setActorInfo(info: ActorInfo): Promise<void>;
12
+ private static requireActorInfo;
13
+ private static requireActorSigner;
14
+ private static signActorToken;
6
15
  private static isValidActorId;
16
+ private static assertActorKeyJwk;
17
+ private static assertActorPrivateKey;
18
+ private static assertActorPublicKey;
7
19
  static schema: <Schema extends SchemaDefinition>(schema: Schema) => Schema;
8
20
  static register: typeof register;
9
21
  static text: typeof text;
@@ -38,8 +50,11 @@ export declare class Dacument<S extends SchemaDefinition> {
38
50
  private readonly opLog;
39
51
  private readonly opTokens;
40
52
  private readonly verifiedOps;
53
+ private readonly opIndexByToken;
54
+ private readonly actorSigByToken;
41
55
  private readonly appliedTokens;
42
56
  private currentRole;
57
+ private resetState;
43
58
  private readonly revokedCrdtByField;
44
59
  private readonly deleteStampsByField;
45
60
  private readonly tombstoneStampsByField;
@@ -49,8 +64,14 @@ export declare class Dacument<S extends SchemaDefinition> {
49
64
  private readonly ackByActor;
50
65
  private suppressMerge;
51
66
  private ackScheduled;
67
+ private actorKeyPublishPending;
52
68
  private lastGcBarrier;
53
69
  private snapshotFieldValues;
70
+ private resetError;
71
+ private assertNotReset;
72
+ private currentRoleFor;
73
+ private roleAt;
74
+ private recordActorSig;
54
75
  readonly acl: {
55
76
  setRole: (actorId: string, role: Role) => void;
56
77
  getRole: (actorId: string) => Role;
@@ -68,11 +89,23 @@ export declare class Dacument<S extends SchemaDefinition> {
68
89
  removeEventListener<K extends keyof DacumentEventMap>(type: K, listener: (event: DacumentEventMap[K]) => void): void;
69
90
  flush(): Promise<void>;
70
91
  snapshot(): DocSnapshot;
92
+ getResetState(): ResetStateInfo | null;
93
+ selfRevoke(): void;
94
+ accessReset(options?: {
95
+ reason?: string;
96
+ }): Promise<{
97
+ newDoc: DacumentDoc<S>;
98
+ oldDocOps: SignedOp[];
99
+ newDocSnapshot: DocSnapshot;
100
+ roleKeys: RoleKeys;
101
+ }>;
102
+ verifyActorIntegrity(options?: VerifyActorIntegrityOptions): Promise<VerificationResult>;
71
103
  merge(input: SignedOp | SignedOp[] | string | string[]): Promise<{
72
104
  accepted: SignedOp[];
73
105
  rejected: number;
74
106
  }>;
75
107
  private rebuildFromVerified;
108
+ private maybePublishActorKey;
76
109
  private ack;
77
110
  private scheduleAck;
78
111
  private computeGcBarrier;
@@ -102,6 +135,8 @@ export declare class Dacument<S extends SchemaDefinition> {
102
135
  private commitRecordMutation;
103
136
  private capturePatches;
104
137
  private queueLocalOp;
138
+ private queueActorOp;
139
+ private applyResetPayload;
105
140
  private applyRemotePayload;
106
141
  private applyAclPayload;
107
142
  private applyRegisterPayload;
@@ -125,9 +160,11 @@ export declare class Dacument<S extends SchemaDefinition> {
125
160
  private recordValue;
126
161
  private mapValue;
127
162
  private fieldValue;
163
+ private materializeSchema;
128
164
  private emitEvent;
129
165
  private emitMerge;
130
166
  private emitRevoked;
167
+ private emitReset;
131
168
  private emitError;
132
169
  private canWriteField;
133
170
  private canWriteAcl;
@@ -140,3 +177,4 @@ export declare class Dacument<S extends SchemaDefinition> {
140
177
  private assertSchemaKeys;
141
178
  }
142
179
  export type DacumentDoc<S extends SchemaDefinition> = Dacument<S> & DocFieldAccess<S>;
180
+ export {};