dacument 1.1.0 → 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
@@ -152,12 +152,34 @@ Snapshots do not include schema or schema ids; callers must supply the schema on
152
152
  - `doc.addEventListener("merge", handler)` emits `{ actor, target, method, data }`.
153
153
  - `doc.addEventListener("error", handler)` emits signing/verification errors.
154
154
  - `doc.addEventListener("revoked", handler)` fires when the current actor is revoked.
155
+ - `doc.addEventListener("reset", handler)` emits `{ oldDocId, newDocId, ts, by, reason }`.
155
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`).
156
159
  - `await doc.flush()` waits for pending signatures so all local ops are emitted.
157
160
  - `doc.snapshot()` returns a loadable op log (`{ docId, roleKeys, ops }`).
158
161
  - `await doc.verifyActorIntegrity(...)` verifies per-actor signatures on demand.
159
162
  - Revoked actors cannot snapshot; reads are masked to initial values.
160
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
+
161
183
  ## Actor identity (cold path)
162
184
 
163
185
  Every op may include an `actorSig` (detached ES256 signature over the op token).
@@ -1,4 +1,10 @@
1
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
9
  private static actorInfo?;
4
10
  private static actorSigner?;
@@ -48,6 +54,7 @@ export declare class Dacument<S extends SchemaDefinition> {
48
54
  private readonly actorSigByToken;
49
55
  private readonly appliedTokens;
50
56
  private currentRole;
57
+ private resetState;
51
58
  private readonly revokedCrdtByField;
52
59
  private readonly deleteStampsByField;
53
60
  private readonly tombstoneStampsByField;
@@ -60,6 +67,10 @@ export declare class Dacument<S extends SchemaDefinition> {
60
67
  private actorKeyPublishPending;
61
68
  private lastGcBarrier;
62
69
  private snapshotFieldValues;
70
+ private resetError;
71
+ private assertNotReset;
72
+ private currentRoleFor;
73
+ private roleAt;
63
74
  private recordActorSig;
64
75
  readonly acl: {
65
76
  setRole: (actorId: string, role: Role) => void;
@@ -78,7 +89,16 @@ export declare class Dacument<S extends SchemaDefinition> {
78
89
  removeEventListener<K extends keyof DacumentEventMap>(type: K, listener: (event: DacumentEventMap[K]) => void): void;
79
90
  flush(): Promise<void>;
80
91
  snapshot(): DocSnapshot;
92
+ getResetState(): ResetStateInfo | null;
81
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
+ }>;
82
102
  verifyActorIntegrity(options?: VerifyActorIntegrityOptions): Promise<VerificationResult>;
83
103
  merge(input: SignedOp | SignedOp[] | string | string[]): Promise<{
84
104
  accepted: SignedOp[];
@@ -116,6 +136,7 @@ export declare class Dacument<S extends SchemaDefinition> {
116
136
  private capturePatches;
117
137
  private queueLocalOp;
118
138
  private queueActorOp;
139
+ private applyResetPayload;
119
140
  private applyRemotePayload;
120
141
  private applyAclPayload;
121
142
  private applyRegisterPayload;
@@ -139,9 +160,11 @@ export declare class Dacument<S extends SchemaDefinition> {
139
160
  private recordValue;
140
161
  private mapValue;
141
162
  private fieldValue;
163
+ private materializeSchema;
142
164
  private emitEvent;
143
165
  private emitMerge;
144
166
  private emitRevoked;
167
+ private emitReset;
145
168
  private emitError;
146
169
  private canWriteField;
147
170
  private canWriteAcl;
@@ -154,3 +177,4 @@ export declare class Dacument<S extends SchemaDefinition> {
154
177
  private assertSchemaKeys;
155
178
  }
156
179
  export type DacumentDoc<S extends SchemaDefinition> = Dacument<S> & DocFieldAccess<S>;
180
+ export {};
@@ -21,6 +21,17 @@ function isObject(value) {
21
21
  function isStringArray(value) {
22
22
  return Array.isArray(value) && value.every((entry) => typeof entry === "string");
23
23
  }
24
+ function isValidNonceId(value) {
25
+ if (typeof value !== "string")
26
+ return false;
27
+ try {
28
+ const bytes = Bytes.fromBase64UrlString(value);
29
+ return bytes.byteLength === 32 && value.length === 43;
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
24
35
  function stableKey(value) {
25
36
  if (value === null)
26
37
  return "null";
@@ -83,6 +94,17 @@ function isAckPatch(value) {
83
94
  typeof seen.logical === "number" &&
84
95
  typeof seen.clockId === "string");
85
96
  }
97
+ function isResetPatch(value) {
98
+ if (!isObject(value))
99
+ return false;
100
+ if (typeof value.newDocId !== "string")
101
+ return false;
102
+ if (!isValidNonceId(value.newDocId))
103
+ return false;
104
+ if ("reason" in value && value.reason !== undefined && typeof value.reason !== "string")
105
+ return false;
106
+ return true;
107
+ }
86
108
  function isPatchEnvelope(value) {
87
109
  return isObject(value) && Array.isArray(value.nodes);
88
110
  }
@@ -174,15 +196,7 @@ export class Dacument {
174
196
  return Bytes.toBase64UrlString(signature);
175
197
  }
176
198
  static isValidActorId(actorId) {
177
- if (typeof actorId !== "string")
178
- return false;
179
- try {
180
- const bytes = Bytes.fromBase64UrlString(actorId);
181
- return bytes.byteLength === 32 && actorId.length === 43;
182
- }
183
- catch {
184
- return false;
185
- }
199
+ return isValidNonceId(actorId);
186
200
  }
187
201
  static assertActorKeyJwk(jwk, label) {
188
202
  if (!jwk || typeof jwk !== "object")
@@ -472,6 +486,7 @@ export class Dacument {
472
486
  actorSigByToken = new Map();
473
487
  appliedTokens = new Set();
474
488
  currentRole;
489
+ resetState = null;
475
490
  revokedCrdtByField = new Map();
476
491
  deleteStampsByField = new Map();
477
492
  tombstoneStampsByField = new Map();
@@ -489,6 +504,24 @@ export class Dacument {
489
504
  values.set(key, this.fieldValue(key));
490
505
  return values;
491
506
  }
507
+ resetError() {
508
+ const newDocId = this.resetState?.newDocId ?? "unknown";
509
+ return new Error(`Dacument is reset/deprecated. Use newDocId: ${newDocId}`);
510
+ }
511
+ assertNotReset() {
512
+ if (this.resetState)
513
+ throw this.resetError();
514
+ }
515
+ currentRoleFor(actorId) {
516
+ if (this.resetState)
517
+ return "revoked";
518
+ return this.aclLog.currentRole(actorId);
519
+ }
520
+ roleAt(actorId, stamp) {
521
+ if (this.resetState && compareHLC(stamp, this.resetState.ts) > 0)
522
+ return "revoked";
523
+ return this.aclLog.roleAt(actorId, stamp);
524
+ }
492
525
  recordActorSig(token, actorSig) {
493
526
  if (!actorSig || this.actorSigByToken.has(token))
494
527
  return;
@@ -517,11 +550,11 @@ export class Dacument {
517
550
  }
518
551
  this.acl = {
519
552
  setRole: (actorId, role) => this.setRole(actorId, role),
520
- getRole: (actorId) => this.aclLog.currentRole(actorId),
553
+ getRole: (actorId) => this.currentRoleFor(actorId),
521
554
  knownActors: () => this.aclLog.knownActors(),
522
555
  snapshot: () => this.aclLog.snapshot(),
523
556
  };
524
- this.currentRole = this.aclLog.currentRole(this.actorId);
557
+ this.currentRole = this.currentRoleFor(this.actorId);
525
558
  return new Proxy(this, {
526
559
  get: (target, property, receiver) => {
527
560
  if (typeof property !== "string")
@@ -588,7 +621,7 @@ export class Dacument {
588
621
  await Promise.all([...this.pending]);
589
622
  }
590
623
  snapshot() {
591
- if (this.isRevoked())
624
+ if (this.isRevoked() && !this.resetState)
592
625
  throw new Error("Dacument: revoked actors cannot snapshot");
593
626
  const ops = this.opLog.map((op) => {
594
627
  const actorSig = this.actorSigByToken.get(op.token);
@@ -600,9 +633,20 @@ export class Dacument {
600
633
  ops,
601
634
  };
602
635
  }
636
+ getResetState() {
637
+ return this.resetState
638
+ ? {
639
+ ts: this.resetState.ts,
640
+ by: this.resetState.by,
641
+ newDocId: this.resetState.newDocId,
642
+ reason: this.resetState.reason,
643
+ }
644
+ : null;
645
+ }
603
646
  selfRevoke() {
647
+ this.assertNotReset();
604
648
  const stamp = this.clock.next();
605
- const role = this.aclLog.roleAt(this.actorId, stamp);
649
+ const role = this.roleAt(this.actorId, stamp);
606
650
  if (role === "revoked")
607
651
  return;
608
652
  const actorInfo = Dacument.requireActorInfo();
@@ -629,6 +673,52 @@ export class Dacument {
629
673
  }
630
674
  this.queueActorOp(payload);
631
675
  }
676
+ async accessReset(options = {}) {
677
+ this.assertNotReset();
678
+ const stamp = this.clock.next();
679
+ const role = this.roleAt(this.actorId, stamp);
680
+ if (role !== "owner")
681
+ throw new Error("Dacument: only owner can accessReset");
682
+ if (!this.roleKey)
683
+ throw new Error("Dacument: missing owner private key");
684
+ const schema = this.materializeSchema();
685
+ const created = await Dacument.create({ schema });
686
+ const newDoc = await Dacument.load({
687
+ schema,
688
+ roleKey: created.roleKeys.owner.privateKey,
689
+ snapshot: created.snapshot,
690
+ });
691
+ const patch = {
692
+ newDocId: created.docId,
693
+ };
694
+ if (options.reason)
695
+ patch.reason = options.reason;
696
+ const payload = {
697
+ iss: this.actorId,
698
+ sub: this.docId,
699
+ iat: nowSeconds(),
700
+ stamp,
701
+ kind: "reset",
702
+ schema: this.schemaId,
703
+ patch,
704
+ };
705
+ const header = {
706
+ alg: "ES256",
707
+ typ: TOKEN_TYP,
708
+ kid: `${this.actorId}:owner`,
709
+ };
710
+ const token = await signToken(this.roleKey, header, payload);
711
+ const actorSig = await Dacument.signActorToken(token);
712
+ const oldDocOps = [{ token, actorSig }];
713
+ this.emitEvent("change", { type: "change", ops: oldDocOps });
714
+ await this.merge(oldDocOps);
715
+ return {
716
+ newDoc,
717
+ oldDocOps,
718
+ newDocSnapshot: created.snapshot,
719
+ roleKeys: created.roleKeys,
720
+ };
721
+ }
632
722
  async verifyActorIntegrity(options = {}) {
633
723
  const input = options.token !== undefined
634
724
  ? [options.token]
@@ -831,7 +921,7 @@ export class Dacument {
831
921
  const entry = this.aclLog.currentEntry(this.actorId);
832
922
  this.emitRevoked(prevRole, entry?.by ?? this.actorId, entry?.stamp ?? this.clock.current);
833
923
  }
834
- if (appliedNonAck)
924
+ if (appliedNonAck && !this.resetState)
835
925
  this.scheduleAck();
836
926
  this.maybeGc();
837
927
  this.maybePublishActorKey();
@@ -847,6 +937,8 @@ export class Dacument {
847
937
  this.tombstoneStampsByField.clear();
848
938
  this.deleteNodeStampsByField.clear();
849
939
  this.revokedCrdtByField.clear();
940
+ this.resetState = null;
941
+ let resetStamp = null;
850
942
  for (const state of this.fields.values()) {
851
943
  state.crdt = createEmptyField(state.schema);
852
944
  }
@@ -864,8 +956,21 @@ export class Dacument {
864
956
  return left.token < right.token ? -1 : 1;
865
957
  });
866
958
  for (const { token, payload, signerRole } of ops) {
959
+ if (resetStamp && compareHLC(payload.stamp, resetStamp) > 0)
960
+ continue;
867
961
  let allowed = false;
868
- if (payload.kind === "acl.set") {
962
+ const isReset = payload.kind === "reset";
963
+ if (isReset) {
964
+ if (this.resetState)
965
+ continue;
966
+ if (!isResetPatch(payload.patch))
967
+ continue;
968
+ const roleAt = this.roleAt(payload.iss, payload.stamp);
969
+ if (signerRole === "owner" && roleAt === "owner") {
970
+ allowed = true;
971
+ }
972
+ }
973
+ else if (payload.kind === "acl.set") {
869
974
  const patch = isAclPatch(payload.patch) ? payload.patch : null;
870
975
  if (!patch)
871
976
  continue;
@@ -876,7 +981,7 @@ export class Dacument {
876
981
  allowed = true;
877
982
  }
878
983
  else {
879
- const roleAt = this.aclLog.roleAt(payload.iss, payload.stamp);
984
+ const roleAt = this.roleAt(payload.iss, payload.stamp);
880
985
  const isSelfRevoke = patch.target === payload.iss && patch.role === "revoked";
881
986
  const targetKey = this.aclLog.publicKeyAt(patch.target, payload.stamp);
882
987
  const isSelfKeyUpdate = patch.target === payload.iss &&
@@ -912,7 +1017,7 @@ export class Dacument {
912
1017
  }
913
1018
  }
914
1019
  else {
915
- const roleAt = this.aclLog.roleAt(payload.iss, payload.stamp);
1020
+ const roleAt = this.roleAt(payload.iss, payload.stamp);
916
1021
  if (payload.kind === "ack") {
917
1022
  if (roleAt === "revoked")
918
1023
  continue;
@@ -930,19 +1035,23 @@ export class Dacument {
930
1035
  const emit = !previousApplied.has(token);
931
1036
  this.suppressMerge = !emit;
932
1037
  try {
933
- const applied = this.applyRemotePayload(payload, signerRole);
1038
+ const applied = isReset
1039
+ ? this.applyResetPayload(payload, emit)
1040
+ : this.applyRemotePayload(payload, signerRole);
934
1041
  if (!applied)
935
1042
  continue;
936
1043
  }
937
1044
  finally {
938
1045
  this.suppressMerge = false;
939
1046
  }
1047
+ if (isReset)
1048
+ resetStamp = payload.stamp;
940
1049
  this.appliedTokens.add(token);
941
1050
  invalidated.delete(token);
942
1051
  if (emit && payload.kind !== "ack")
943
1052
  appliedNonAck = true;
944
1053
  }
945
- this.currentRole = this.aclLog.currentRole(this.actorId);
1054
+ this.currentRole = this.currentRoleFor(this.actorId);
946
1055
  if (invalidated.size > 0 &&
947
1056
  options?.beforeValues &&
948
1057
  options.diffActor &&
@@ -952,6 +1061,8 @@ export class Dacument {
952
1061
  return { appliedNonAck };
953
1062
  }
954
1063
  maybePublishActorKey() {
1064
+ if (this.resetState)
1065
+ return;
955
1066
  const entry = this.aclLog.currentEntry(this.actorId);
956
1067
  if (entry?.publicKeyJwk) {
957
1068
  this.actorKeyPublishPending = false;
@@ -985,8 +1096,9 @@ export class Dacument {
985
1096
  });
986
1097
  }
987
1098
  ack() {
1099
+ this.assertNotReset();
988
1100
  const stamp = this.clock.next();
989
- const role = this.aclLog.roleAt(this.actorId, stamp);
1101
+ const role = this.roleAt(this.actorId, stamp);
990
1102
  if (role === "revoked")
991
1103
  throw new Error("Dacument: revoked actors cannot acknowledge");
992
1104
  const seen = this.clock.current;
@@ -1006,6 +1118,8 @@ export class Dacument {
1006
1118
  return;
1007
1119
  if (this.currentRole === "revoked")
1008
1120
  return;
1121
+ if (this.resetState)
1122
+ return;
1009
1123
  this.ackScheduled = true;
1010
1124
  queueMicrotask(() => {
1011
1125
  this.ackScheduled = false;
@@ -1034,6 +1148,8 @@ export class Dacument {
1034
1148
  return barrier;
1035
1149
  }
1036
1150
  maybeGc() {
1151
+ if (this.resetState)
1152
+ return;
1037
1153
  const barrier = this.computeGcBarrier();
1038
1154
  if (!barrier)
1039
1155
  return;
@@ -1160,8 +1276,9 @@ export class Dacument {
1160
1276
  throw new Error(`Dacument: invalid value for '${field}'`);
1161
1277
  if (schema.regex && typeof value === "string" && !schema.regex.test(value))
1162
1278
  throw new Error(`Dacument: '${field}' failed regex`);
1279
+ this.assertNotReset();
1163
1280
  const stamp = this.clock.next();
1164
- const role = this.aclLog.roleAt(this.actorId, stamp);
1281
+ const role = this.roleAt(this.actorId, stamp);
1165
1282
  if (!this.canWriteField(role))
1166
1283
  throw new Error(`Dacument: role '${role}' cannot write '${field}'`);
1167
1284
  this.queueLocalOp({
@@ -1340,7 +1457,7 @@ export class Dacument {
1340
1457
  insertAt(index, value) {
1341
1458
  doc.assertValueType(field, value);
1342
1459
  const stamp = doc.clock.next();
1343
- const role = doc.aclLog.roleAt(doc.actorId, stamp);
1460
+ const role = doc.roleAt(doc.actorId, stamp);
1344
1461
  doc.assertWritable(field, role);
1345
1462
  const shadow = doc.shadowFor(field, state);
1346
1463
  const { patches, result } = doc.capturePatches((listener) => shadow.onChange(listener), () => shadow.insertAt(index, value));
@@ -1360,7 +1477,7 @@ export class Dacument {
1360
1477
  },
1361
1478
  deleteAt(index) {
1362
1479
  const stamp = doc.clock.next();
1363
- const role = doc.aclLog.roleAt(doc.actorId, stamp);
1480
+ const role = doc.roleAt(doc.actorId, stamp);
1364
1481
  doc.assertWritable(field, role);
1365
1482
  const shadow = doc.shadowFor(field, state);
1366
1483
  const { patches, result } = doc.capturePatches((listener) => shadow.onChange(listener), () => shadow.deleteAt(index));
@@ -1568,7 +1685,7 @@ export class Dacument {
1568
1685
  commitArrayMutation(field, mutate) {
1569
1686
  const state = this.fields.get(field);
1570
1687
  const stamp = this.clock.next();
1571
- const role = this.aclLog.roleAt(this.actorId, stamp);
1688
+ const role = this.roleAt(this.actorId, stamp);
1572
1689
  this.assertWritable(field, role);
1573
1690
  const shadow = this.shadowFor(field, state);
1574
1691
  const { patches, result } = this.capturePatches((listener) => shadow.onChange(listener), () => mutate(shadow));
@@ -1589,7 +1706,7 @@ export class Dacument {
1589
1706
  commitSetMutation(field, mutate) {
1590
1707
  const state = this.fields.get(field);
1591
1708
  const stamp = this.clock.next();
1592
- const role = this.aclLog.roleAt(this.actorId, stamp);
1709
+ const role = this.roleAt(this.actorId, stamp);
1593
1710
  this.assertWritable(field, role);
1594
1711
  const shadow = this.shadowFor(field, state);
1595
1712
  const { patches, result } = this.capturePatches((listener) => shadow.onChange(listener), () => mutate(shadow));
@@ -1610,7 +1727,7 @@ export class Dacument {
1610
1727
  commitMapMutation(field, mutate) {
1611
1728
  const state = this.fields.get(field);
1612
1729
  const stamp = this.clock.next();
1613
- const role = this.aclLog.roleAt(this.actorId, stamp);
1730
+ const role = this.roleAt(this.actorId, stamp);
1614
1731
  this.assertWritable(field, role);
1615
1732
  const shadow = this.shadowFor(field, state);
1616
1733
  const { patches, result } = this.capturePatches((listener) => shadow.onChange(listener), () => mutate(shadow));
@@ -1631,7 +1748,7 @@ export class Dacument {
1631
1748
  commitRecordMutation(field, mutate) {
1632
1749
  const state = this.fields.get(field);
1633
1750
  const stamp = this.clock.next();
1634
- const role = this.aclLog.roleAt(this.actorId, stamp);
1751
+ const role = this.roleAt(this.actorId, stamp);
1635
1752
  this.assertWritable(field, role);
1636
1753
  const shadow = this.shadowFor(field, state);
1637
1754
  const { patches, result } = this.capturePatches((listener) => shadow.onChange(listener), () => mutate(shadow));
@@ -1662,6 +1779,7 @@ export class Dacument {
1662
1779
  return { patches, result };
1663
1780
  }
1664
1781
  queueLocalOp(payload, role) {
1782
+ this.assertNotReset();
1665
1783
  if (payload.kind === "ack") {
1666
1784
  const header = { alg: "none", typ: TOKEN_TYP };
1667
1785
  const token = encodeToken(header, payload);
@@ -1684,6 +1802,7 @@ export class Dacument {
1684
1802
  promise.finally(() => this.pending.delete(promise));
1685
1803
  }
1686
1804
  queueActorOp(payload, onError) {
1805
+ this.assertNotReset();
1687
1806
  const actorInfo = Dacument.requireActorInfo();
1688
1807
  const header = {
1689
1808
  alg: "ES256",
@@ -1703,6 +1822,23 @@ export class Dacument {
1703
1822
  this.pending.add(promise);
1704
1823
  promise.finally(() => this.pending.delete(promise));
1705
1824
  }
1825
+ applyResetPayload(payload, emit) {
1826
+ if (!isResetPatch(payload.patch))
1827
+ return false;
1828
+ if (this.resetState)
1829
+ return false;
1830
+ this.clock.observe(payload.stamp);
1831
+ const patch = payload.patch;
1832
+ this.resetState = {
1833
+ ts: payload.stamp,
1834
+ by: payload.iss,
1835
+ newDocId: patch.newDocId,
1836
+ reason: patch.reason,
1837
+ };
1838
+ if (emit)
1839
+ this.emitReset(this.resetState);
1840
+ return true;
1841
+ }
1706
1842
  applyRemotePayload(payload, signerRole) {
1707
1843
  this.clock.observe(payload.stamp);
1708
1844
  if (payload.kind === "ack") {
@@ -2168,8 +2304,9 @@ export class Dacument {
2168
2304
  return count;
2169
2305
  }
2170
2306
  setRole(actorId, role) {
2307
+ this.assertNotReset();
2171
2308
  const stamp = this.clock.next();
2172
- const signerRole = this.aclLog.roleAt(this.actorId, stamp);
2309
+ const signerRole = this.roleAt(this.actorId, stamp);
2173
2310
  if (!this.canWriteAclTarget(signerRole, role, actorId, stamp))
2174
2311
  throw new Error(`Dacument: role '${signerRole}' cannot grant '${role}'`);
2175
2312
  const assignmentId = uuidv7();
@@ -2222,6 +2359,62 @@ export class Dacument {
2222
2359
  return this.recordValue(crdt);
2223
2360
  }
2224
2361
  }
2362
+ materializeSchema() {
2363
+ const output = {};
2364
+ for (const [field, schema] of Object.entries(this.schema)) {
2365
+ const current = this.fieldValue(field);
2366
+ if (schema.crdt === "register") {
2367
+ const next = { ...schema };
2368
+ if (isValueOfType(current, schema.jsType)) {
2369
+ if (schema.regex &&
2370
+ typeof current === "string" &&
2371
+ !schema.regex.test(current))
2372
+ throw new Error(`Dacument.accessReset: '${field}' failed regex`);
2373
+ next.initial = current;
2374
+ }
2375
+ else {
2376
+ delete next.initial;
2377
+ }
2378
+ output[field] = next;
2379
+ continue;
2380
+ }
2381
+ if (schema.crdt === "text") {
2382
+ output[field] = {
2383
+ ...schema,
2384
+ initial: typeof current === "string" ? current : "",
2385
+ };
2386
+ continue;
2387
+ }
2388
+ if (schema.crdt === "array") {
2389
+ output[field] = {
2390
+ ...schema,
2391
+ initial: Array.isArray(current) ? current : [],
2392
+ };
2393
+ continue;
2394
+ }
2395
+ if (schema.crdt === "set") {
2396
+ output[field] = {
2397
+ ...schema,
2398
+ initial: Array.isArray(current) ? current : [],
2399
+ };
2400
+ continue;
2401
+ }
2402
+ if (schema.crdt === "map") {
2403
+ output[field] = {
2404
+ ...schema,
2405
+ initial: Array.isArray(current) ? current : [],
2406
+ };
2407
+ continue;
2408
+ }
2409
+ if (schema.crdt === "record") {
2410
+ output[field] =
2411
+ current && isObject(current) && !Array.isArray(current)
2412
+ ? { ...schema, initial: current }
2413
+ : { ...schema, initial: {} };
2414
+ }
2415
+ }
2416
+ return output;
2417
+ }
2225
2418
  emitEvent(type, event) {
2226
2419
  const listeners = this.eventListeners.get(type);
2227
2420
  if (!listeners)
@@ -2245,6 +2438,16 @@ export class Dacument {
2245
2438
  stamp,
2246
2439
  });
2247
2440
  }
2441
+ emitReset(payload) {
2442
+ this.emitEvent("reset", {
2443
+ type: "reset",
2444
+ oldDocId: this.docId,
2445
+ newDocId: payload.newDocId,
2446
+ ts: payload.ts,
2447
+ by: payload.by,
2448
+ reason: payload.reason,
2449
+ });
2450
+ }
2248
2451
  emitError(error) {
2249
2452
  this.emitEvent("error", { type: "error", error });
2250
2453
  }
@@ -2262,13 +2465,14 @@ export class Dacument {
2262
2465
  if (!this.canWriteAcl(role, targetRole))
2263
2466
  return false;
2264
2467
  if (role === "manager") {
2265
- const targetRoleAt = this.aclLog.roleAt(targetActorId, stamp);
2468
+ const targetRoleAt = this.roleAt(targetActorId, stamp);
2266
2469
  if (targetRoleAt === "owner")
2267
2470
  return false;
2268
2471
  }
2269
2472
  return true;
2270
2473
  }
2271
2474
  assertWritable(field, role) {
2475
+ this.assertNotReset();
2272
2476
  if (!this.canWriteField(role))
2273
2477
  throw new Error(`Dacument: role '${role}' cannot write '${field}'`);
2274
2478
  }
@@ -69,7 +69,7 @@ export type RecordSchema<T extends JsTypeName = JsTypeName> = {
69
69
  export type FieldSchema = RegisterSchema | TextSchema | ArraySchema | SetSchema | MapSchema | RecordSchema;
70
70
  export type SchemaDefinition = Record<string, FieldSchema>;
71
71
  export type SchemaId = string;
72
- export type OpKind = "acl.set" | "register.set" | "text.patch" | "array.patch" | "map.patch" | "set.patch" | "record.patch" | "ack";
72
+ export type OpKind = "acl.set" | "register.set" | "text.patch" | "array.patch" | "map.patch" | "set.patch" | "record.patch" | "ack" | "reset";
73
73
  export type OpPayload = {
74
74
  iss: string;
75
75
  sub: string;
@@ -84,6 +84,16 @@ export type SignedOp = {
84
84
  token: string;
85
85
  actorSig?: string;
86
86
  };
87
+ export type ResetPatch = {
88
+ newDocId: string;
89
+ reason?: string;
90
+ };
91
+ export type ResetState = {
92
+ ts: HLCStamp;
93
+ by: string;
94
+ newDocId: string;
95
+ reason?: string;
96
+ };
87
97
  export type DacumentChangeEvent = {
88
98
  type: "change";
89
99
  ops: SignedOp[];
@@ -106,11 +116,20 @@ export type DacumentRevokedEvent = {
106
116
  by: string;
107
117
  stamp: HLCStamp;
108
118
  };
119
+ export type DacumentResetEvent = {
120
+ type: "reset";
121
+ oldDocId: string;
122
+ newDocId: string;
123
+ ts: HLCStamp;
124
+ by: string;
125
+ reason?: string;
126
+ };
109
127
  export type DacumentEventMap = {
110
128
  change: DacumentChangeEvent;
111
129
  merge: DacumentMergeEvent;
112
130
  error: DacumentErrorEvent;
113
131
  revoked: DacumentRevokedEvent;
132
+ reset: DacumentResetEvent;
114
133
  };
115
134
  export type AclAssignment = {
116
135
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dacument",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Schema-driven CRDT document with signed ops, role-based ACLs, and optional per-actor verification.",
5
5
  "keywords": [
6
6
  "crdt",