dacument 1.1.0 → 1.2.1

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.
@@ -9,7 +9,7 @@ import { CRSet } from "../CRSet/class.js";
9
9
  import { CRText } from "../CRText/class.js";
10
10
  import { AclLog } from "./acl.js";
11
11
  import { HLC, compareHLC } from "./clock.js";
12
- import { decodeToken, encodeToken, signToken, validateActorKeyPair, verifyDetached, verifyToken, } from "./crypto.js";
12
+ import { decodeToken, signToken, validateActorKeyPair, verifyDetached, verifyToken, } from "./crypto.js";
13
13
  import { array, map, record, register, set, text, isJsValue, isValueOfType, schemaIdInput, } from "./types.js";
14
14
  const TOKEN_TYP = "DACOP";
15
15
  function nowSeconds() {
@@ -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
  }
@@ -147,15 +169,34 @@ function toPublicRoleKeys(roleKeys) {
147
169
  export class Dacument {
148
170
  static actorInfo;
149
171
  static actorSigner;
172
+ static actorInfoPrevious;
150
173
  static async setActorInfo(info) {
151
- if (Dacument.actorInfo)
152
- return;
174
+ const existing = Dacument.actorInfo;
175
+ if (existing) {
176
+ if (info.id !== existing.id)
177
+ throw new Error("Dacument.setActorInfo: actor id already set");
178
+ const samePrivate = jwkEquals(info.privateKeyJwk, existing.privateKeyJwk);
179
+ const samePublic = jwkEquals(info.publicKeyJwk, existing.publicKeyJwk);
180
+ if (samePrivate && samePublic)
181
+ return;
182
+ if (!info.currentPrivateKeyJwk || !info.currentPublicKeyJwk)
183
+ throw new Error("Dacument.setActorInfo: current keys required to update actor info");
184
+ if (!jwkEquals(info.currentPrivateKeyJwk, existing.privateKeyJwk) ||
185
+ !jwkEquals(info.currentPublicKeyJwk, existing.publicKeyJwk))
186
+ throw new Error("Dacument.setActorInfo: current keys do not match existing actor info");
187
+ }
153
188
  if (!Dacument.isValidActorId(info.id))
154
189
  throw new Error("Dacument.setActorInfo: id must be 256-bit base64url");
155
190
  Dacument.assertActorPrivateKey(info.privateKeyJwk);
156
191
  Dacument.assertActorPublicKey(info.publicKeyJwk);
157
192
  await validateActorKeyPair(info.privateKeyJwk, info.publicKeyJwk);
158
- Dacument.actorInfo = info;
193
+ if (existing)
194
+ Dacument.actorInfoPrevious = existing;
195
+ Dacument.actorInfo = {
196
+ id: info.id,
197
+ privateKeyJwk: info.privateKeyJwk,
198
+ publicKeyJwk: info.publicKeyJwk,
199
+ };
159
200
  Dacument.actorSigner = new SigningAgent(info.privateKeyJwk);
160
201
  }
161
202
  static requireActorInfo() {
@@ -168,21 +209,30 @@ export class Dacument {
168
209
  throw new Error("Dacument: actor info not set; call Dacument.setActorInfo()");
169
210
  return Dacument.actorSigner;
170
211
  }
171
- static async signActorToken(token) {
172
- const signer = Dacument.requireActorSigner();
212
+ static async signActorToken(token, privateKeyJwk) {
213
+ const current = Dacument.actorInfo;
214
+ const signer = privateKeyJwk &&
215
+ current &&
216
+ jwkEquals(privateKeyJwk, current.privateKeyJwk)
217
+ ? Dacument.requireActorSigner()
218
+ : privateKeyJwk
219
+ ? new SigningAgent(privateKeyJwk)
220
+ : Dacument.requireActorSigner();
173
221
  const signature = await signer.sign(Bytes.fromString(token));
174
222
  return Bytes.toBase64UrlString(signature);
175
223
  }
176
224
  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
- }
225
+ return isValidNonceId(actorId);
226
+ }
227
+ static actorInfoForPublicKey(publicKeyJwk) {
228
+ if (!publicKeyJwk)
229
+ return null;
230
+ if (Dacument.actorInfo && jwkEquals(publicKeyJwk, Dacument.actorInfo.publicKeyJwk))
231
+ return Dacument.actorInfo;
232
+ if (Dacument.actorInfoPrevious &&
233
+ jwkEquals(publicKeyJwk, Dacument.actorInfoPrevious.publicKeyJwk))
234
+ return Dacument.actorInfoPrevious;
235
+ return null;
186
236
  }
187
237
  static assertActorKeyJwk(jwk, label) {
188
238
  if (!jwk || typeof jwk !== "object")
@@ -472,6 +522,7 @@ export class Dacument {
472
522
  actorSigByToken = new Map();
473
523
  appliedTokens = new Set();
474
524
  currentRole;
525
+ resetState = null;
475
526
  revokedCrdtByField = new Map();
476
527
  deleteStampsByField = new Map();
477
528
  tombstoneStampsByField = new Map();
@@ -489,6 +540,24 @@ export class Dacument {
489
540
  values.set(key, this.fieldValue(key));
490
541
  return values;
491
542
  }
543
+ resetError() {
544
+ const newDocId = this.resetState?.newDocId ?? "unknown";
545
+ return new Error(`Dacument is reset/deprecated. Use newDocId: ${newDocId}`);
546
+ }
547
+ assertNotReset() {
548
+ if (this.resetState)
549
+ throw this.resetError();
550
+ }
551
+ currentRoleFor(actorId) {
552
+ if (this.resetState)
553
+ return "revoked";
554
+ return this.aclLog.currentRole(actorId);
555
+ }
556
+ roleAt(actorId, stamp) {
557
+ if (this.resetState && compareHLC(stamp, this.resetState.ts) > 0)
558
+ return "revoked";
559
+ return this.aclLog.roleAt(actorId, stamp);
560
+ }
492
561
  recordActorSig(token, actorSig) {
493
562
  if (!actorSig || this.actorSigByToken.has(token))
494
563
  return;
@@ -517,11 +586,11 @@ export class Dacument {
517
586
  }
518
587
  this.acl = {
519
588
  setRole: (actorId, role) => this.setRole(actorId, role),
520
- getRole: (actorId) => this.aclLog.currentRole(actorId),
589
+ getRole: (actorId) => this.currentRoleFor(actorId),
521
590
  knownActors: () => this.aclLog.knownActors(),
522
591
  snapshot: () => this.aclLog.snapshot(),
523
592
  };
524
- this.currentRole = this.aclLog.currentRole(this.actorId);
593
+ this.currentRole = this.currentRoleFor(this.actorId);
525
594
  return new Proxy(this, {
526
595
  get: (target, property, receiver) => {
527
596
  if (typeof property !== "string")
@@ -588,7 +657,7 @@ export class Dacument {
588
657
  await Promise.all([...this.pending]);
589
658
  }
590
659
  snapshot() {
591
- if (this.isRevoked())
660
+ if (this.isRevoked() && !this.resetState)
592
661
  throw new Error("Dacument: revoked actors cannot snapshot");
593
662
  const ops = this.opLog.map((op) => {
594
663
  const actorSig = this.actorSigByToken.get(op.token);
@@ -600,9 +669,20 @@ export class Dacument {
600
669
  ops,
601
670
  };
602
671
  }
672
+ getResetState() {
673
+ return this.resetState
674
+ ? {
675
+ ts: this.resetState.ts,
676
+ by: this.resetState.by,
677
+ newDocId: this.resetState.newDocId,
678
+ reason: this.resetState.reason,
679
+ }
680
+ : null;
681
+ }
603
682
  selfRevoke() {
683
+ this.assertNotReset();
604
684
  const stamp = this.clock.next();
605
- const role = this.aclLog.roleAt(this.actorId, stamp);
685
+ const role = this.roleAt(this.actorId, stamp);
606
686
  if (role === "revoked")
607
687
  return;
608
688
  const actorInfo = Dacument.requireActorInfo();
@@ -629,6 +709,52 @@ export class Dacument {
629
709
  }
630
710
  this.queueActorOp(payload);
631
711
  }
712
+ async accessReset(options = {}) {
713
+ this.assertNotReset();
714
+ const stamp = this.clock.next();
715
+ const role = this.roleAt(this.actorId, stamp);
716
+ if (role !== "owner")
717
+ throw new Error("Dacument: only owner can accessReset");
718
+ if (!this.roleKey)
719
+ throw new Error("Dacument: missing owner private key");
720
+ const schema = this.materializeSchema();
721
+ const created = await Dacument.create({ schema });
722
+ const newDoc = await Dacument.load({
723
+ schema,
724
+ roleKey: created.roleKeys.owner.privateKey,
725
+ snapshot: created.snapshot,
726
+ });
727
+ const patch = {
728
+ newDocId: created.docId,
729
+ };
730
+ if (options.reason)
731
+ patch.reason = options.reason;
732
+ const payload = {
733
+ iss: this.actorId,
734
+ sub: this.docId,
735
+ iat: nowSeconds(),
736
+ stamp,
737
+ kind: "reset",
738
+ schema: this.schemaId,
739
+ patch,
740
+ };
741
+ const header = {
742
+ alg: "ES256",
743
+ typ: TOKEN_TYP,
744
+ kid: `${this.actorId}:owner`,
745
+ };
746
+ const token = await signToken(this.roleKey, header, payload);
747
+ const actorSig = await Dacument.signActorToken(token);
748
+ const oldDocOps = [{ token, actorSig }];
749
+ this.emitEvent("change", { type: "change", ops: oldDocOps });
750
+ await this.merge(oldDocOps);
751
+ return {
752
+ newDoc,
753
+ oldDocOps,
754
+ newDocSnapshot: created.snapshot,
755
+ roleKeys: created.roleKeys,
756
+ };
757
+ }
632
758
  async verifyActorIntegrity(options = {}) {
633
759
  const input = options.token !== undefined
634
760
  ? [options.token]
@@ -715,33 +841,32 @@ export class Dacument {
715
841
  rejected++;
716
842
  continue;
717
843
  }
718
- const isUnsignedAck = decoded.header.alg === "none" &&
719
- payload.kind === "ack" &&
720
- decoded.header.typ === TOKEN_TYP;
721
- if (decoded.header.alg === "none" && !isUnsignedAck) {
722
- rejected++;
723
- continue;
724
- }
725
- if (payload.kind === "ack" && decoded.header.alg !== "none") {
844
+ if (decoded.header.alg === "none") {
726
845
  rejected++;
727
846
  continue;
728
847
  }
729
848
  let stored = this.verifiedOps.get(token);
730
849
  if (!stored) {
731
- if (isUnsignedAck) {
732
- stored = { payload, signerRole: null };
850
+ const signerKind = parseSignerKind(decoded.header.kid, payload.iss);
851
+ if (!signerKind) {
852
+ rejected++;
853
+ continue;
733
854
  }
734
- else {
735
- const signerKind = parseSignerKind(decoded.header.kid, payload.iss);
736
- if (!signerKind) {
737
- rejected++;
738
- continue;
739
- }
740
- if (signerKind === "actor") {
741
- if (payload.kind !== "acl.set") {
855
+ if (signerKind === "actor") {
856
+ if (payload.kind === "ack") {
857
+ const publicKey = this.aclLog.publicKeyAt(payload.iss, payload.stamp);
858
+ if (!publicKey) {
859
+ rejected++;
860
+ continue;
861
+ }
862
+ const verified = await verifyToken(publicKey, token, TOKEN_TYP);
863
+ if (!verified) {
742
864
  rejected++;
743
865
  continue;
744
866
  }
867
+ stored = { payload, signerRole: "actor" };
868
+ }
869
+ else if (payload.kind === "acl.set") {
745
870
  const patch = isAclPatch(payload.patch) ? payload.patch : null;
746
871
  if (!patch || patch.target !== payload.iss) {
747
872
  rejected++;
@@ -754,12 +879,6 @@ export class Dacument {
754
879
  continue;
755
880
  }
756
881
  const existingKey = this.aclLog.publicKeyAt(payload.iss, payload.stamp);
757
- if (existingKey &&
758
- patch.publicKeyJwk &&
759
- !jwkEquals(existingKey, patch.publicKeyJwk)) {
760
- rejected++;
761
- continue;
762
- }
763
882
  const publicKey = existingKey ?? patch.publicKeyJwk;
764
883
  if (!publicKey) {
765
884
  rejected++;
@@ -773,14 +892,22 @@ export class Dacument {
773
892
  stored = { payload, signerRole: "actor" };
774
893
  }
775
894
  else {
776
- const publicKey = this.roleKeys[signerKind];
777
- const verified = await verifyToken(publicKey, token, TOKEN_TYP);
778
- if (!verified) {
779
- rejected++;
780
- continue;
781
- }
782
- stored = { payload, signerRole: signerKind };
895
+ rejected++;
896
+ continue;
897
+ }
898
+ }
899
+ else {
900
+ if (payload.kind === "ack") {
901
+ rejected++;
902
+ continue;
903
+ }
904
+ const publicKey = this.roleKeys[signerKind];
905
+ const verified = await verifyToken(publicKey, token, TOKEN_TYP);
906
+ if (!verified) {
907
+ rejected++;
908
+ continue;
783
909
  }
910
+ stored = { payload, signerRole: signerKind };
784
911
  }
785
912
  this.verifiedOps.set(token, stored);
786
913
  if (!this.opTokens.has(token)) {
@@ -831,7 +958,7 @@ export class Dacument {
831
958
  const entry = this.aclLog.currentEntry(this.actorId);
832
959
  this.emitRevoked(prevRole, entry?.by ?? this.actorId, entry?.stamp ?? this.clock.current);
833
960
  }
834
- if (appliedNonAck)
961
+ if (appliedNonAck && !this.resetState)
835
962
  this.scheduleAck();
836
963
  this.maybeGc();
837
964
  this.maybePublishActorKey();
@@ -847,6 +974,8 @@ export class Dacument {
847
974
  this.tombstoneStampsByField.clear();
848
975
  this.deleteNodeStampsByField.clear();
849
976
  this.revokedCrdtByField.clear();
977
+ this.resetState = null;
978
+ let resetStamp = null;
850
979
  for (const state of this.fields.values()) {
851
980
  state.crdt = createEmptyField(state.schema);
852
981
  }
@@ -864,8 +993,21 @@ export class Dacument {
864
993
  return left.token < right.token ? -1 : 1;
865
994
  });
866
995
  for (const { token, payload, signerRole } of ops) {
996
+ if (resetStamp && compareHLC(payload.stamp, resetStamp) > 0)
997
+ continue;
867
998
  let allowed = false;
868
- if (payload.kind === "acl.set") {
999
+ const isReset = payload.kind === "reset";
1000
+ if (isReset) {
1001
+ if (this.resetState)
1002
+ continue;
1003
+ if (!isResetPatch(payload.patch))
1004
+ continue;
1005
+ const roleAt = this.roleAt(payload.iss, payload.stamp);
1006
+ if (signerRole === "owner" && roleAt === "owner") {
1007
+ allowed = true;
1008
+ }
1009
+ }
1010
+ else if (payload.kind === "acl.set") {
869
1011
  const patch = isAclPatch(payload.patch) ? payload.patch : null;
870
1012
  if (!patch)
871
1013
  continue;
@@ -876,19 +1018,19 @@ export class Dacument {
876
1018
  allowed = true;
877
1019
  }
878
1020
  else {
879
- const roleAt = this.aclLog.roleAt(payload.iss, payload.stamp);
880
- const isSelfRevoke = patch.target === payload.iss && patch.role === "revoked";
1021
+ const roleAt = this.roleAt(payload.iss, payload.stamp);
1022
+ const isSelf = patch.target === payload.iss;
1023
+ const isSelfRevoke = isSelf && patch.role === "revoked";
881
1024
  const targetKey = this.aclLog.publicKeyAt(patch.target, payload.stamp);
882
- const isSelfKeyUpdate = patch.target === payload.iss &&
1025
+ const keyMismatch = Boolean(patch.publicKeyJwk) &&
1026
+ Boolean(targetKey) &&
1027
+ !jwkEquals(targetKey, patch.publicKeyJwk);
1028
+ const isSelfKeyUpdate = isSelf &&
883
1029
  patch.publicKeyJwk &&
884
1030
  patch.role === roleAt &&
885
- roleAt !== "revoked" &&
886
- (!targetKey || jwkEquals(targetKey, patch.publicKeyJwk));
887
- if (patch.publicKeyJwk &&
888
- targetKey &&
889
- !jwkEquals(targetKey, patch.publicKeyJwk)) {
1031
+ roleAt !== "revoked";
1032
+ if (keyMismatch && signerRole !== "actor")
890
1033
  continue;
891
- }
892
1034
  if (isSelfRevoke) {
893
1035
  if (signerRole === "actor") {
894
1036
  allowed = true;
@@ -905,18 +1047,18 @@ export class Dacument {
905
1047
  if (this.canWriteAclTarget(signerRole, patch.role, patch.target, payload.stamp)) {
906
1048
  allowed = true;
907
1049
  }
908
- else if (isSelfKeyUpdate) {
1050
+ else if (isSelfKeyUpdate && !keyMismatch) {
909
1051
  allowed = true;
910
1052
  }
911
1053
  }
912
1054
  }
913
1055
  }
914
1056
  else {
915
- const roleAt = this.aclLog.roleAt(payload.iss, payload.stamp);
1057
+ const roleAt = this.roleAt(payload.iss, payload.stamp);
916
1058
  if (payload.kind === "ack") {
917
1059
  if (roleAt === "revoked")
918
1060
  continue;
919
- if (signerRole !== null)
1061
+ if (signerRole !== "actor")
920
1062
  continue;
921
1063
  allowed = true;
922
1064
  }
@@ -930,19 +1072,23 @@ export class Dacument {
930
1072
  const emit = !previousApplied.has(token);
931
1073
  this.suppressMerge = !emit;
932
1074
  try {
933
- const applied = this.applyRemotePayload(payload, signerRole);
1075
+ const applied = isReset
1076
+ ? this.applyResetPayload(payload, emit)
1077
+ : this.applyRemotePayload(payload, signerRole);
934
1078
  if (!applied)
935
1079
  continue;
936
1080
  }
937
1081
  finally {
938
1082
  this.suppressMerge = false;
939
1083
  }
1084
+ if (isReset)
1085
+ resetStamp = payload.stamp;
940
1086
  this.appliedTokens.add(token);
941
1087
  invalidated.delete(token);
942
1088
  if (emit && payload.kind !== "ack")
943
1089
  appliedNonAck = true;
944
1090
  }
945
- this.currentRole = this.aclLog.currentRole(this.actorId);
1091
+ this.currentRole = this.currentRoleFor(this.actorId);
946
1092
  if (invalidated.size > 0 &&
947
1093
  options?.beforeValues &&
948
1094
  options.diffActor &&
@@ -952,10 +1098,15 @@ export class Dacument {
952
1098
  return { appliedNonAck };
953
1099
  }
954
1100
  maybePublishActorKey() {
1101
+ if (this.resetState)
1102
+ return;
955
1103
  const entry = this.aclLog.currentEntry(this.actorId);
956
1104
  if (entry?.publicKeyJwk) {
957
- this.actorKeyPublishPending = false;
958
- return;
1105
+ const actorInfo = Dacument.requireActorInfo();
1106
+ if (jwkEquals(entry.publicKeyJwk, actorInfo.publicKeyJwk)) {
1107
+ this.actorKeyPublishPending = false;
1108
+ return;
1109
+ }
959
1110
  }
960
1111
  if (this.actorKeyPublishPending)
961
1112
  return;
@@ -964,6 +1115,13 @@ export class Dacument {
964
1115
  if (!entry)
965
1116
  return;
966
1117
  const actorInfo = Dacument.requireActorInfo();
1118
+ const signerInfo = entry.publicKeyJwk
1119
+ ? Dacument.actorInfoForPublicKey(entry.publicKeyJwk)
1120
+ : actorInfo;
1121
+ if (entry.publicKeyJwk && !signerInfo) {
1122
+ this.emitError(new Error("Dacument: actor key mismatch; update requires current key material"));
1123
+ return;
1124
+ }
967
1125
  const stamp = this.clock.next();
968
1126
  const payload = {
969
1127
  iss: this.actorId,
@@ -980,18 +1138,37 @@ export class Dacument {
980
1138
  },
981
1139
  };
982
1140
  this.actorKeyPublishPending = true;
983
- this.queueActorOp(payload, () => {
984
- this.actorKeyPublishPending = false;
1141
+ this.queueActorOp(payload, {
1142
+ signer: (signerInfo ?? actorInfo).privateKeyJwk,
1143
+ onError: () => {
1144
+ this.actorKeyPublishPending = false;
1145
+ },
985
1146
  });
986
1147
  }
1148
+ actorSignatureKey() {
1149
+ const entry = this.aclLog.currentEntry(this.actorId);
1150
+ if (!entry?.publicKeyJwk)
1151
+ return null;
1152
+ const actorInfo = Dacument.actorInfoForPublicKey(entry.publicKeyJwk);
1153
+ return actorInfo?.privateKeyJwk ?? null;
1154
+ }
987
1155
  ack() {
1156
+ this.assertNotReset();
988
1157
  const stamp = this.clock.next();
989
- const role = this.aclLog.roleAt(this.actorId, stamp);
1158
+ const role = this.roleAt(this.actorId, stamp);
990
1159
  if (role === "revoked")
991
1160
  throw new Error("Dacument: revoked actors cannot acknowledge");
1161
+ const entry = this.aclLog.currentEntry(this.actorId);
1162
+ if (!entry?.publicKeyJwk)
1163
+ return;
1164
+ const actorInfo = Dacument.actorInfoForPublicKey(entry.publicKeyJwk);
1165
+ if (!actorInfo) {
1166
+ this.emitError(new Error("Dacument: actor key not available to sign ack"));
1167
+ return;
1168
+ }
992
1169
  const seen = this.clock.current;
993
1170
  this.ackByActor.set(this.actorId, seen);
994
- this.queueLocalOp({
1171
+ this.queueActorOp({
995
1172
  iss: this.actorId,
996
1173
  sub: this.docId,
997
1174
  iat: nowSeconds(),
@@ -999,13 +1176,15 @@ export class Dacument {
999
1176
  kind: "ack",
1000
1177
  schema: this.schemaId,
1001
1178
  patch: { seen },
1002
- }, role);
1179
+ }, { signer: actorInfo.privateKeyJwk });
1003
1180
  }
1004
1181
  scheduleAck() {
1005
1182
  if (this.ackScheduled)
1006
1183
  return;
1007
1184
  if (this.currentRole === "revoked")
1008
1185
  return;
1186
+ if (this.resetState)
1187
+ return;
1009
1188
  this.ackScheduled = true;
1010
1189
  queueMicrotask(() => {
1011
1190
  this.ackScheduled = false;
@@ -1034,6 +1213,8 @@ export class Dacument {
1034
1213
  return barrier;
1035
1214
  }
1036
1215
  maybeGc() {
1216
+ if (this.resetState)
1217
+ return;
1037
1218
  const barrier = this.computeGcBarrier();
1038
1219
  if (!barrier)
1039
1220
  return;
@@ -1160,8 +1341,9 @@ export class Dacument {
1160
1341
  throw new Error(`Dacument: invalid value for '${field}'`);
1161
1342
  if (schema.regex && typeof value === "string" && !schema.regex.test(value))
1162
1343
  throw new Error(`Dacument: '${field}' failed regex`);
1344
+ this.assertNotReset();
1163
1345
  const stamp = this.clock.next();
1164
- const role = this.aclLog.roleAt(this.actorId, stamp);
1346
+ const role = this.roleAt(this.actorId, stamp);
1165
1347
  if (!this.canWriteField(role))
1166
1348
  throw new Error(`Dacument: role '${role}' cannot write '${field}'`);
1167
1349
  this.queueLocalOp({
@@ -1340,7 +1522,7 @@ export class Dacument {
1340
1522
  insertAt(index, value) {
1341
1523
  doc.assertValueType(field, value);
1342
1524
  const stamp = doc.clock.next();
1343
- const role = doc.aclLog.roleAt(doc.actorId, stamp);
1525
+ const role = doc.roleAt(doc.actorId, stamp);
1344
1526
  doc.assertWritable(field, role);
1345
1527
  const shadow = doc.shadowFor(field, state);
1346
1528
  const { patches, result } = doc.capturePatches((listener) => shadow.onChange(listener), () => shadow.insertAt(index, value));
@@ -1360,7 +1542,7 @@ export class Dacument {
1360
1542
  },
1361
1543
  deleteAt(index) {
1362
1544
  const stamp = doc.clock.next();
1363
- const role = doc.aclLog.roleAt(doc.actorId, stamp);
1545
+ const role = doc.roleAt(doc.actorId, stamp);
1364
1546
  doc.assertWritable(field, role);
1365
1547
  const shadow = doc.shadowFor(field, state);
1366
1548
  const { patches, result } = doc.capturePatches((listener) => shadow.onChange(listener), () => shadow.deleteAt(index));
@@ -1568,7 +1750,7 @@ export class Dacument {
1568
1750
  commitArrayMutation(field, mutate) {
1569
1751
  const state = this.fields.get(field);
1570
1752
  const stamp = this.clock.next();
1571
- const role = this.aclLog.roleAt(this.actorId, stamp);
1753
+ const role = this.roleAt(this.actorId, stamp);
1572
1754
  this.assertWritable(field, role);
1573
1755
  const shadow = this.shadowFor(field, state);
1574
1756
  const { patches, result } = this.capturePatches((listener) => shadow.onChange(listener), () => mutate(shadow));
@@ -1589,7 +1771,7 @@ export class Dacument {
1589
1771
  commitSetMutation(field, mutate) {
1590
1772
  const state = this.fields.get(field);
1591
1773
  const stamp = this.clock.next();
1592
- const role = this.aclLog.roleAt(this.actorId, stamp);
1774
+ const role = this.roleAt(this.actorId, stamp);
1593
1775
  this.assertWritable(field, role);
1594
1776
  const shadow = this.shadowFor(field, state);
1595
1777
  const { patches, result } = this.capturePatches((listener) => shadow.onChange(listener), () => mutate(shadow));
@@ -1610,7 +1792,7 @@ export class Dacument {
1610
1792
  commitMapMutation(field, mutate) {
1611
1793
  const state = this.fields.get(field);
1612
1794
  const stamp = this.clock.next();
1613
- const role = this.aclLog.roleAt(this.actorId, stamp);
1795
+ const role = this.roleAt(this.actorId, stamp);
1614
1796
  this.assertWritable(field, role);
1615
1797
  const shadow = this.shadowFor(field, state);
1616
1798
  const { patches, result } = this.capturePatches((listener) => shadow.onChange(listener), () => mutate(shadow));
@@ -1631,7 +1813,7 @@ export class Dacument {
1631
1813
  commitRecordMutation(field, mutate) {
1632
1814
  const state = this.fields.get(field);
1633
1815
  const stamp = this.clock.next();
1634
- const role = this.aclLog.roleAt(this.actorId, stamp);
1816
+ const role = this.roleAt(this.actorId, stamp);
1635
1817
  this.assertWritable(field, role);
1636
1818
  const shadow = this.shadowFor(field, state);
1637
1819
  const { patches, result } = this.capturePatches((listener) => shadow.onChange(listener), () => mutate(shadow));
@@ -1662,11 +1844,9 @@ export class Dacument {
1662
1844
  return { patches, result };
1663
1845
  }
1664
1846
  queueLocalOp(payload, role) {
1847
+ this.assertNotReset();
1665
1848
  if (payload.kind === "ack") {
1666
- const header = { alg: "none", typ: TOKEN_TYP };
1667
- const token = encodeToken(header, payload);
1668
- this.emitEvent("change", { type: "change", ops: [{ token }] });
1669
- return;
1849
+ throw new Error("Dacument: ack ops must be actor-signed");
1670
1850
  }
1671
1851
  if (!roleNeedsKey(role))
1672
1852
  throw new Error(`Dacument: role '${role}' cannot sign ops`);
@@ -1675,34 +1855,56 @@ export class Dacument {
1675
1855
  const header = { alg: "ES256", typ: TOKEN_TYP, kid: `${payload.iss}:${role}` };
1676
1856
  const promise = signToken(this.roleKey, header, payload)
1677
1857
  .then(async (token) => {
1678
- const actorSig = await Dacument.signActorToken(token);
1679
- const op = { token, actorSig };
1858
+ const actorSigKey = this.actorSignatureKey();
1859
+ const actorSig = actorSigKey
1860
+ ? await Dacument.signActorToken(token, actorSigKey)
1861
+ : undefined;
1862
+ const op = actorSig ? { token, actorSig } : { token };
1680
1863
  this.emitEvent("change", { type: "change", ops: [op] });
1681
1864
  })
1682
1865
  .catch((error) => this.emitError(error instanceof Error ? error : new Error(String(error))));
1683
1866
  this.pending.add(promise);
1684
1867
  promise.finally(() => this.pending.delete(promise));
1685
1868
  }
1686
- queueActorOp(payload, onError) {
1869
+ queueActorOp(payload, options) {
1870
+ this.assertNotReset();
1687
1871
  const actorInfo = Dacument.requireActorInfo();
1872
+ const signingKey = options?.signer ?? actorInfo.privateKeyJwk;
1688
1873
  const header = {
1689
1874
  alg: "ES256",
1690
1875
  typ: TOKEN_TYP,
1691
1876
  kid: `${payload.iss}:actor`,
1692
1877
  };
1693
- const promise = signToken(actorInfo.privateKeyJwk, header, payload)
1878
+ const promise = signToken(signingKey, header, payload)
1694
1879
  .then(async (token) => {
1695
- const actorSig = await Dacument.signActorToken(token);
1880
+ const actorSig = await Dacument.signActorToken(token, signingKey);
1696
1881
  const op = { token, actorSig };
1697
1882
  this.emitEvent("change", { type: "change", ops: [op] });
1698
1883
  })
1699
1884
  .catch((error) => {
1700
- onError?.();
1885
+ options?.onError?.();
1701
1886
  this.emitError(error instanceof Error ? error : new Error(String(error)));
1702
1887
  });
1703
1888
  this.pending.add(promise);
1704
1889
  promise.finally(() => this.pending.delete(promise));
1705
1890
  }
1891
+ applyResetPayload(payload, emit) {
1892
+ if (!isResetPatch(payload.patch))
1893
+ return false;
1894
+ if (this.resetState)
1895
+ return false;
1896
+ this.clock.observe(payload.stamp);
1897
+ const patch = payload.patch;
1898
+ this.resetState = {
1899
+ ts: payload.stamp,
1900
+ by: payload.iss,
1901
+ newDocId: patch.newDocId,
1902
+ reason: patch.reason,
1903
+ };
1904
+ if (emit)
1905
+ this.emitReset(this.resetState);
1906
+ return true;
1907
+ }
1706
1908
  applyRemotePayload(payload, signerRole) {
1707
1909
  this.clock.observe(payload.stamp);
1708
1910
  if (payload.kind === "ack") {
@@ -2168,8 +2370,9 @@ export class Dacument {
2168
2370
  return count;
2169
2371
  }
2170
2372
  setRole(actorId, role) {
2373
+ this.assertNotReset();
2171
2374
  const stamp = this.clock.next();
2172
- const signerRole = this.aclLog.roleAt(this.actorId, stamp);
2375
+ const signerRole = this.roleAt(this.actorId, stamp);
2173
2376
  if (!this.canWriteAclTarget(signerRole, role, actorId, stamp))
2174
2377
  throw new Error(`Dacument: role '${signerRole}' cannot grant '${role}'`);
2175
2378
  const assignmentId = uuidv7();
@@ -2222,6 +2425,62 @@ export class Dacument {
2222
2425
  return this.recordValue(crdt);
2223
2426
  }
2224
2427
  }
2428
+ materializeSchema() {
2429
+ const output = {};
2430
+ for (const [field, schema] of Object.entries(this.schema)) {
2431
+ const current = this.fieldValue(field);
2432
+ if (schema.crdt === "register") {
2433
+ const next = { ...schema };
2434
+ if (isValueOfType(current, schema.jsType)) {
2435
+ if (schema.regex &&
2436
+ typeof current === "string" &&
2437
+ !schema.regex.test(current))
2438
+ throw new Error(`Dacument.accessReset: '${field}' failed regex`);
2439
+ next.initial = current;
2440
+ }
2441
+ else {
2442
+ delete next.initial;
2443
+ }
2444
+ output[field] = next;
2445
+ continue;
2446
+ }
2447
+ if (schema.crdt === "text") {
2448
+ output[field] = {
2449
+ ...schema,
2450
+ initial: typeof current === "string" ? current : "",
2451
+ };
2452
+ continue;
2453
+ }
2454
+ if (schema.crdt === "array") {
2455
+ output[field] = {
2456
+ ...schema,
2457
+ initial: Array.isArray(current) ? current : [],
2458
+ };
2459
+ continue;
2460
+ }
2461
+ if (schema.crdt === "set") {
2462
+ output[field] = {
2463
+ ...schema,
2464
+ initial: Array.isArray(current) ? current : [],
2465
+ };
2466
+ continue;
2467
+ }
2468
+ if (schema.crdt === "map") {
2469
+ output[field] = {
2470
+ ...schema,
2471
+ initial: Array.isArray(current) ? current : [],
2472
+ };
2473
+ continue;
2474
+ }
2475
+ if (schema.crdt === "record") {
2476
+ output[field] =
2477
+ current && isObject(current) && !Array.isArray(current)
2478
+ ? { ...schema, initial: current }
2479
+ : { ...schema, initial: {} };
2480
+ }
2481
+ }
2482
+ return output;
2483
+ }
2225
2484
  emitEvent(type, event) {
2226
2485
  const listeners = this.eventListeners.get(type);
2227
2486
  if (!listeners)
@@ -2245,6 +2504,16 @@ export class Dacument {
2245
2504
  stamp,
2246
2505
  });
2247
2506
  }
2507
+ emitReset(payload) {
2508
+ this.emitEvent("reset", {
2509
+ type: "reset",
2510
+ oldDocId: this.docId,
2511
+ newDocId: payload.newDocId,
2512
+ ts: payload.ts,
2513
+ by: payload.by,
2514
+ reason: payload.reason,
2515
+ });
2516
+ }
2248
2517
  emitError(error) {
2249
2518
  this.emitEvent("error", { type: "error", error });
2250
2519
  }
@@ -2262,13 +2531,14 @@ export class Dacument {
2262
2531
  if (!this.canWriteAcl(role, targetRole))
2263
2532
  return false;
2264
2533
  if (role === "manager") {
2265
- const targetRoleAt = this.aclLog.roleAt(targetActorId, stamp);
2534
+ const targetRoleAt = this.roleAt(targetActorId, stamp);
2266
2535
  if (targetRoleAt === "owner")
2267
2536
  return false;
2268
2537
  }
2269
2538
  return true;
2270
2539
  }
2271
2540
  assertWritable(field, role) {
2541
+ this.assertNotReset();
2272
2542
  if (!this.canWriteField(role))
2273
2543
  throw new Error(`Dacument: role '${role}' cannot write '${field}'`);
2274
2544
  }