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.
@@ -1,5 +1,5 @@
1
1
  import { Bytes, generateNonce } from "bytecodec";
2
- import { generateSignPair } from "zeyra";
2
+ import { SigningAgent, generateSignPair } from "zeyra";
3
3
  import { v7 as uuidv7 } from "uuid";
4
4
  import { CRArray } from "../CRArray/class.js";
5
5
  import { CRMap } from "../CRMap/class.js";
@@ -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, verifyToken } from "./crypto.js";
12
+ import { decodeToken, encodeToken, 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";
@@ -35,6 +46,15 @@ function stableKey(value) {
35
46
  }
36
47
  return JSON.stringify(value);
37
48
  }
49
+ function normalizeJwk(jwk) {
50
+ const entries = Object.entries(jwk).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
51
+ return JSON.stringify(Object.fromEntries(entries));
52
+ }
53
+ function jwkEquals(left, right) {
54
+ if (!left || !right)
55
+ return false;
56
+ return normalizeJwk(left) === normalizeJwk(right);
57
+ }
38
58
  function isDagNode(node) {
39
59
  if (!isObject(node))
40
60
  return false;
@@ -55,6 +75,13 @@ function isAclPatch(value) {
55
75
  return false;
56
76
  if (typeof value.role !== "string")
57
77
  return false;
78
+ if ("publicKeyJwk" in value && value.publicKeyJwk !== undefined) {
79
+ if (!isObject(value.publicKeyJwk))
80
+ return false;
81
+ const jwk = value.publicKeyJwk;
82
+ if (jwk.kty && jwk.kty !== "EC")
83
+ return false;
84
+ }
58
85
  return true;
59
86
  }
60
87
  function isAckPatch(value) {
@@ -67,6 +94,17 @@ function isAckPatch(value) {
67
94
  typeof seen.logical === "number" &&
68
95
  typeof seen.clockId === "string");
69
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
+ }
70
108
  function isPatchEnvelope(value) {
71
109
  return isObject(value) && Array.isArray(value.nodes);
72
110
  }
@@ -99,7 +137,7 @@ function createEmptyField(crdt) {
99
137
  function roleNeedsKey(role) {
100
138
  return role === "owner" || role === "manager" || role === "editor";
101
139
  }
102
- function parseSignerRole(kid, issuer) {
140
+ function parseSignerKind(kid, issuer) {
103
141
  if (!kid)
104
142
  return null;
105
143
  const [kidIssuer, role] = kid.split(":");
@@ -107,6 +145,8 @@ function parseSignerRole(kid, issuer) {
107
145
  return null;
108
146
  if (role === "owner" || role === "manager" || role === "editor")
109
147
  return role;
148
+ if (role === "actor")
149
+ return "actor";
110
150
  return null;
111
151
  }
112
152
  async function generateRoleKeys() {
@@ -127,32 +167,57 @@ function toPublicRoleKeys(roleKeys) {
127
167
  };
128
168
  }
129
169
  export class Dacument {
130
- static actorId;
131
- static setActorId(actorId) {
132
- if (Dacument.actorId)
170
+ static actorInfo;
171
+ static actorSigner;
172
+ static async setActorInfo(info) {
173
+ if (Dacument.actorInfo)
133
174
  return;
134
- if (!Dacument.isValidActorId(actorId))
135
- throw new Error("Dacument.setActorId: actorId must be 256-bit base64url");
136
- Dacument.actorId = actorId;
137
- }
138
- static requireActorId() {
139
- if (!Dacument.actorId)
140
- throw new Error("Dacument: actorId not set; call Dacument.setActorId()");
141
- return Dacument.actorId;
175
+ if (!Dacument.isValidActorId(info.id))
176
+ throw new Error("Dacument.setActorInfo: id must be 256-bit base64url");
177
+ Dacument.assertActorPrivateKey(info.privateKeyJwk);
178
+ Dacument.assertActorPublicKey(info.publicKeyJwk);
179
+ await validateActorKeyPair(info.privateKeyJwk, info.publicKeyJwk);
180
+ Dacument.actorInfo = info;
181
+ Dacument.actorSigner = new SigningAgent(info.privateKeyJwk);
182
+ }
183
+ static requireActorInfo() {
184
+ if (!Dacument.actorInfo)
185
+ throw new Error("Dacument: actor info not set; call Dacument.setActorInfo()");
186
+ return Dacument.actorInfo;
187
+ }
188
+ static requireActorSigner() {
189
+ if (!Dacument.actorSigner)
190
+ throw new Error("Dacument: actor info not set; call Dacument.setActorInfo()");
191
+ return Dacument.actorSigner;
192
+ }
193
+ static async signActorToken(token) {
194
+ const signer = Dacument.requireActorSigner();
195
+ const signature = await signer.sign(Bytes.fromString(token));
196
+ return Bytes.toBase64UrlString(signature);
142
197
  }
143
198
  static isValidActorId(actorId) {
144
- if (typeof actorId !== "string")
145
- return false;
146
- try {
147
- const bytes = Bytes.fromBase64UrlString(actorId);
148
- return bytes.byteLength === 32 && actorId.length === 43;
149
- }
150
- catch {
151
- return false;
152
- }
199
+ return isValidNonceId(actorId);
200
+ }
201
+ static assertActorKeyJwk(jwk, label) {
202
+ if (!jwk || typeof jwk !== "object")
203
+ throw new Error(`Dacument.setActorInfo: ${label} must be a JWK object`);
204
+ if (jwk.kty !== "EC")
205
+ throw new Error(`Dacument.setActorInfo: ${label} must be EC (P-256)`);
206
+ if (jwk.crv && jwk.crv !== "P-256")
207
+ throw new Error(`Dacument.setActorInfo: ${label} must use P-256`);
208
+ if (jwk.alg && jwk.alg !== "ES256")
209
+ throw new Error(`Dacument.setActorInfo: ${label} must use ES256`);
210
+ }
211
+ static assertActorPrivateKey(jwk) {
212
+ Dacument.assertActorKeyJwk(jwk, "privateKeyJwk");
213
+ if (!jwk.d)
214
+ throw new Error("Dacument.setActorInfo: privateKeyJwk must include 'd'");
215
+ }
216
+ static assertActorPublicKey(jwk) {
217
+ Dacument.assertActorKeyJwk(jwk, "publicKeyJwk");
153
218
  }
154
219
  static schema = (schema) => {
155
- Dacument.requireActorId();
220
+ Dacument.requireActorInfo();
156
221
  return schema;
157
222
  };
158
223
  static register = register;
@@ -173,7 +238,8 @@ export class Dacument {
173
238
  return Bytes.toBase64UrlString(new Uint8Array(digest));
174
239
  }
175
240
  static async create(params) {
176
- const ownerId = Dacument.requireActorId();
241
+ const ownerInfo = Dacument.requireActorInfo();
242
+ const ownerId = ownerInfo.id;
177
243
  const docId = params.docId ?? generateNonce();
178
244
  const schemaId = await Dacument.computeSchemaId(params.schema);
179
245
  const roleKeys = await generateRoleKeys();
@@ -198,7 +264,8 @@ export class Dacument {
198
264
  };
199
265
  const sign = async (payload) => {
200
266
  const token = await signToken(roleKeys.owner.privateKey, header, payload);
201
- ops.push({ token });
267
+ const actorSig = await Dacument.signActorToken(token);
268
+ ops.push({ token, actorSig });
202
269
  };
203
270
  await sign({
204
271
  iss: ownerId,
@@ -211,6 +278,7 @@ export class Dacument {
211
278
  id: uuidv7(),
212
279
  target: ownerId,
213
280
  role: "owner",
281
+ publicKeyJwk: ownerInfo.publicKeyJwk,
214
282
  },
215
283
  });
216
284
  for (const [field, schema] of Object.entries(params.schema)) {
@@ -390,7 +458,7 @@ export class Dacument {
390
458
  return { docId, schemaId, roleKeys, snapshot };
391
459
  }
392
460
  static async load(params) {
393
- const actorId = Dacument.requireActorId();
461
+ const actorId = Dacument.requireActorInfo().id;
394
462
  const schemaId = await Dacument.computeSchemaId(params.schema);
395
463
  const doc = new Dacument({
396
464
  schema: params.schema,
@@ -414,8 +482,11 @@ export class Dacument {
414
482
  opLog = [];
415
483
  opTokens = new Set();
416
484
  verifiedOps = new Map();
485
+ opIndexByToken = new Map();
486
+ actorSigByToken = new Map();
417
487
  appliedTokens = new Set();
418
488
  currentRole;
489
+ resetState = null;
419
490
  revokedCrdtByField = new Map();
420
491
  deleteStampsByField = new Map();
421
492
  tombstoneStampsByField = new Map();
@@ -425,6 +496,7 @@ export class Dacument {
425
496
  ackByActor = new Map();
426
497
  suppressMerge = false;
427
498
  ackScheduled = false;
499
+ actorKeyPublishPending = false;
428
500
  lastGcBarrier = null;
429
501
  snapshotFieldValues() {
430
502
  const values = new Map();
@@ -432,9 +504,38 @@ export class Dacument {
432
504
  values.set(key, this.fieldValue(key));
433
505
  return values;
434
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
+ }
525
+ recordActorSig(token, actorSig) {
526
+ if (!actorSig || this.actorSigByToken.has(token))
527
+ return;
528
+ this.actorSigByToken.set(token, actorSig);
529
+ const index = this.opIndexByToken.get(token);
530
+ if (index === undefined)
531
+ return;
532
+ const entry = this.opLog[index];
533
+ if (!entry.actorSig)
534
+ entry.actorSig = actorSig;
535
+ }
435
536
  acl;
436
537
  constructor(params) {
437
- const actorId = Dacument.requireActorId();
538
+ const actorId = Dacument.requireActorInfo().id;
438
539
  this.schema = params.schema;
439
540
  this.schemaId = params.schemaId;
440
541
  this.docId = params.docId;
@@ -449,11 +550,11 @@ export class Dacument {
449
550
  }
450
551
  this.acl = {
451
552
  setRole: (actorId, role) => this.setRole(actorId, role),
452
- getRole: (actorId) => this.aclLog.currentRole(actorId),
553
+ getRole: (actorId) => this.currentRoleFor(actorId),
453
554
  knownActors: () => this.aclLog.knownActors(),
454
555
  snapshot: () => this.aclLog.snapshot(),
455
556
  };
456
- this.currentRole = this.aclLog.currentRole(this.actorId);
557
+ this.currentRole = this.currentRoleFor(this.actorId);
457
558
  return new Proxy(this, {
458
559
  get: (target, property, receiver) => {
459
560
  if (typeof property !== "string")
@@ -520,12 +621,163 @@ export class Dacument {
520
621
  await Promise.all([...this.pending]);
521
622
  }
522
623
  snapshot() {
523
- if (this.isRevoked())
624
+ if (this.isRevoked() && !this.resetState)
524
625
  throw new Error("Dacument: revoked actors cannot snapshot");
626
+ const ops = this.opLog.map((op) => {
627
+ const actorSig = this.actorSigByToken.get(op.token);
628
+ return actorSig ? { token: op.token, actorSig } : { token: op.token };
629
+ });
525
630
  return {
526
631
  docId: this.docId,
527
632
  roleKeys: this.roleKeys,
528
- ops: this.opLog.slice(),
633
+ ops,
634
+ };
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
+ }
646
+ selfRevoke() {
647
+ this.assertNotReset();
648
+ const stamp = this.clock.next();
649
+ const role = this.roleAt(this.actorId, stamp);
650
+ if (role === "revoked")
651
+ return;
652
+ const actorInfo = Dacument.requireActorInfo();
653
+ const entry = this.aclLog.currentEntry(this.actorId);
654
+ const patch = {
655
+ id: uuidv7(),
656
+ target: this.actorId,
657
+ role: "revoked",
658
+ };
659
+ if (!entry?.publicKeyJwk)
660
+ patch.publicKeyJwk = actorInfo.publicKeyJwk;
661
+ const payload = {
662
+ iss: this.actorId,
663
+ sub: this.docId,
664
+ iat: nowSeconds(),
665
+ stamp,
666
+ kind: "acl.set",
667
+ schema: this.schemaId,
668
+ patch,
669
+ };
670
+ if (roleNeedsKey(role) && this.roleKey) {
671
+ this.queueLocalOp(payload, role);
672
+ return;
673
+ }
674
+ this.queueActorOp(payload);
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
+ }
722
+ async verifyActorIntegrity(options = {}) {
723
+ const input = options.token !== undefined
724
+ ? [options.token]
725
+ : options.ops ?? options.snapshot?.ops ?? this.opLog;
726
+ let verified = 0;
727
+ let failed = 0;
728
+ let missing = 0;
729
+ const failures = [];
730
+ for (let index = 0; index < input.length; index++) {
731
+ const item = input[index];
732
+ const token = typeof item === "string" ? item : item.token;
733
+ const actorSig = typeof item === "string"
734
+ ? this.actorSigByToken.get(token)
735
+ : item.actorSig ?? this.actorSigByToken.get(token);
736
+ const decoded = decodeToken(token);
737
+ if (!decoded) {
738
+ failed++;
739
+ failures.push({ index, reason: "invalid token" });
740
+ continue;
741
+ }
742
+ const payload = decoded.payload;
743
+ if (!this.isValidPayload(payload)) {
744
+ failed++;
745
+ failures.push({ index, reason: "invalid payload" });
746
+ continue;
747
+ }
748
+ if (!actorSig) {
749
+ missing++;
750
+ continue;
751
+ }
752
+ const publicKey = this.aclLog.publicKeyAt(payload.iss, payload.stamp);
753
+ if (!publicKey) {
754
+ missing++;
755
+ continue;
756
+ }
757
+ try {
758
+ const ok = await verifyDetached(publicKey, token, actorSig);
759
+ if (!ok) {
760
+ failed++;
761
+ failures.push({ index, reason: "actor signature mismatch" });
762
+ continue;
763
+ }
764
+ }
765
+ catch (error) {
766
+ failed++;
767
+ failures.push({
768
+ index,
769
+ reason: error instanceof Error ? error.message : "actor signature error",
770
+ });
771
+ continue;
772
+ }
773
+ verified++;
774
+ }
775
+ return {
776
+ ok: failed === 0,
777
+ verified,
778
+ failed,
779
+ missing,
780
+ failures,
529
781
  };
530
782
  }
531
783
  async merge(input) {
@@ -538,6 +790,7 @@ export class Dacument {
538
790
  let diffStamp = null;
539
791
  for (const item of tokens) {
540
792
  const token = typeof item === "string" ? item : item.token;
793
+ const actorSig = typeof item === "string" ? undefined : item.actorSig;
541
794
  const decoded = decodeToken(token);
542
795
  if (!decoded) {
543
796
  rejected++;
@@ -569,23 +822,66 @@ export class Dacument {
569
822
  stored = { payload, signerRole: null };
570
823
  }
571
824
  else {
572
- const signerRole = parseSignerRole(decoded.header.kid, payload.iss);
573
- if (!signerRole) {
825
+ const signerKind = parseSignerKind(decoded.header.kid, payload.iss);
826
+ if (!signerKind) {
574
827
  rejected++;
575
828
  continue;
576
829
  }
577
- const publicKey = this.roleKeys[signerRole];
578
- const verified = await verifyToken(publicKey, token, TOKEN_TYP);
579
- if (!verified) {
580
- rejected++;
581
- continue;
830
+ if (signerKind === "actor") {
831
+ if (payload.kind !== "acl.set") {
832
+ rejected++;
833
+ continue;
834
+ }
835
+ const patch = isAclPatch(payload.patch) ? payload.patch : null;
836
+ if (!patch || patch.target !== payload.iss) {
837
+ rejected++;
838
+ continue;
839
+ }
840
+ const wantsSelfRevoke = patch.role === "revoked";
841
+ const wantsKeyAttach = Boolean(patch.publicKeyJwk);
842
+ if (!wantsSelfRevoke && !wantsKeyAttach) {
843
+ rejected++;
844
+ continue;
845
+ }
846
+ const existingKey = this.aclLog.publicKeyAt(payload.iss, payload.stamp);
847
+ if (existingKey &&
848
+ patch.publicKeyJwk &&
849
+ !jwkEquals(existingKey, patch.publicKeyJwk)) {
850
+ rejected++;
851
+ continue;
852
+ }
853
+ const publicKey = existingKey ?? patch.publicKeyJwk;
854
+ if (!publicKey) {
855
+ rejected++;
856
+ continue;
857
+ }
858
+ const verified = await verifyToken(publicKey, token, TOKEN_TYP);
859
+ if (!verified) {
860
+ rejected++;
861
+ continue;
862
+ }
863
+ stored = { payload, signerRole: "actor" };
864
+ }
865
+ else {
866
+ const publicKey = this.roleKeys[signerKind];
867
+ const verified = await verifyToken(publicKey, token, TOKEN_TYP);
868
+ if (!verified) {
869
+ rejected++;
870
+ continue;
871
+ }
872
+ stored = { payload, signerRole: signerKind };
582
873
  }
583
- stored = { payload, signerRole };
584
874
  }
585
875
  this.verifiedOps.set(token, stored);
586
876
  if (!this.opTokens.has(token)) {
587
877
  this.opTokens.add(token);
588
- this.opLog.push({ token });
878
+ const opEntry = { token };
879
+ if (typeof actorSig === "string") {
880
+ opEntry.actorSig = actorSig;
881
+ this.actorSigByToken.set(token, actorSig);
882
+ }
883
+ this.opLog.push(opEntry);
884
+ this.opIndexByToken.set(token, this.opLog.length - 1);
589
885
  }
590
886
  sawNewToken = true;
591
887
  if (payload.kind === "acl.set") {
@@ -595,6 +891,7 @@ export class Dacument {
595
891
  }
596
892
  }
597
893
  }
894
+ this.recordActorSig(token, actorSig);
598
895
  decodedOps.push({
599
896
  token,
600
897
  payload: stored.payload,
@@ -605,7 +902,7 @@ export class Dacument {
605
902
  let appliedNonAck = false;
606
903
  if (sawNewToken) {
607
904
  const beforeValues = this.isRevoked() ? undefined : this.snapshotFieldValues();
608
- const result = this.rebuildFromVerified(new Set(this.appliedTokens), {
905
+ const result = await this.rebuildFromVerified(new Set(this.appliedTokens), {
609
906
  beforeValues,
610
907
  diffActor: diffActor ?? this.actorId,
611
908
  });
@@ -624,12 +921,13 @@ export class Dacument {
624
921
  const entry = this.aclLog.currentEntry(this.actorId);
625
922
  this.emitRevoked(prevRole, entry?.by ?? this.actorId, entry?.stamp ?? this.clock.current);
626
923
  }
627
- if (appliedNonAck)
924
+ if (appliedNonAck && !this.resetState)
628
925
  this.scheduleAck();
629
926
  this.maybeGc();
927
+ this.maybePublishActorKey();
630
928
  return { accepted, rejected };
631
929
  }
632
- rebuildFromVerified(previousApplied, options) {
930
+ async rebuildFromVerified(previousApplied, options) {
633
931
  const invalidated = new Set(previousApplied);
634
932
  let appliedNonAck = false;
635
933
  this.aclLog.reset();
@@ -639,6 +937,8 @@ export class Dacument {
639
937
  this.tombstoneStampsByField.clear();
640
938
  this.deleteNodeStampsByField.clear();
641
939
  this.revokedCrdtByField.clear();
940
+ this.resetState = null;
941
+ let resetStamp = null;
642
942
  for (const state of this.fields.values()) {
643
943
  state.crdt = createEmptyField(state.schema);
644
944
  }
@@ -656,27 +956,68 @@ export class Dacument {
656
956
  return left.token < right.token ? -1 : 1;
657
957
  });
658
958
  for (const { token, payload, signerRole } of ops) {
959
+ if (resetStamp && compareHLC(payload.stamp, resetStamp) > 0)
960
+ continue;
659
961
  let allowed = false;
660
- if (payload.kind === "acl.set") {
661
- if (!signerRole)
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") {
974
+ const patch = isAclPatch(payload.patch) ? payload.patch : null;
975
+ if (!patch)
662
976
  continue;
663
977
  if (this.aclLog.isEmpty() &&
664
- isAclPatch(payload.patch) &&
665
- payload.patch.role === "owner" &&
666
- payload.patch.target === payload.iss &&
978
+ patch.role === "owner" &&
979
+ patch.target === payload.iss &&
667
980
  signerRole === "owner") {
668
981
  allowed = true;
669
982
  }
670
983
  else {
671
- const roleAt = this.aclLog.roleAt(payload.iss, payload.stamp);
672
- if (roleAt === signerRole &&
673
- isAclPatch(payload.patch) &&
674
- this.canWriteAclTarget(signerRole, payload.patch.role, payload.patch.target, payload.stamp))
675
- allowed = true;
984
+ const roleAt = this.roleAt(payload.iss, payload.stamp);
985
+ const isSelfRevoke = patch.target === payload.iss && patch.role === "revoked";
986
+ const targetKey = this.aclLog.publicKeyAt(patch.target, payload.stamp);
987
+ const isSelfKeyUpdate = patch.target === payload.iss &&
988
+ patch.publicKeyJwk &&
989
+ patch.role === roleAt &&
990
+ roleAt !== "revoked" &&
991
+ (!targetKey || jwkEquals(targetKey, patch.publicKeyJwk));
992
+ if (patch.publicKeyJwk &&
993
+ targetKey &&
994
+ !jwkEquals(targetKey, patch.publicKeyJwk)) {
995
+ continue;
996
+ }
997
+ if (isSelfRevoke) {
998
+ if (signerRole === "actor") {
999
+ allowed = true;
1000
+ }
1001
+ else if (signerRole && roleAt === signerRole) {
1002
+ allowed = true;
1003
+ }
1004
+ }
1005
+ else if (signerRole === "actor") {
1006
+ if (isSelfKeyUpdate)
1007
+ allowed = true;
1008
+ }
1009
+ else if (signerRole && roleAt === signerRole) {
1010
+ if (this.canWriteAclTarget(signerRole, patch.role, patch.target, payload.stamp)) {
1011
+ allowed = true;
1012
+ }
1013
+ else if (isSelfKeyUpdate) {
1014
+ allowed = true;
1015
+ }
1016
+ }
676
1017
  }
677
1018
  }
678
1019
  else {
679
- const roleAt = this.aclLog.roleAt(payload.iss, payload.stamp);
1020
+ const roleAt = this.roleAt(payload.iss, payload.stamp);
680
1021
  if (payload.kind === "ack") {
681
1022
  if (roleAt === "revoked")
682
1023
  continue;
@@ -694,19 +1035,23 @@ export class Dacument {
694
1035
  const emit = !previousApplied.has(token);
695
1036
  this.suppressMerge = !emit;
696
1037
  try {
697
- const applied = this.applyRemotePayload(payload, signerRole);
1038
+ const applied = isReset
1039
+ ? this.applyResetPayload(payload, emit)
1040
+ : this.applyRemotePayload(payload, signerRole);
698
1041
  if (!applied)
699
1042
  continue;
700
1043
  }
701
1044
  finally {
702
1045
  this.suppressMerge = false;
703
1046
  }
1047
+ if (isReset)
1048
+ resetStamp = payload.stamp;
704
1049
  this.appliedTokens.add(token);
705
1050
  invalidated.delete(token);
706
1051
  if (emit && payload.kind !== "ack")
707
1052
  appliedNonAck = true;
708
1053
  }
709
- this.currentRole = this.aclLog.currentRole(this.actorId);
1054
+ this.currentRole = this.currentRoleFor(this.actorId);
710
1055
  if (invalidated.size > 0 &&
711
1056
  options?.beforeValues &&
712
1057
  options.diffActor &&
@@ -715,9 +1060,45 @@ export class Dacument {
715
1060
  }
716
1061
  return { appliedNonAck };
717
1062
  }
1063
+ maybePublishActorKey() {
1064
+ if (this.resetState)
1065
+ return;
1066
+ const entry = this.aclLog.currentEntry(this.actorId);
1067
+ if (entry?.publicKeyJwk) {
1068
+ this.actorKeyPublishPending = false;
1069
+ return;
1070
+ }
1071
+ if (this.actorKeyPublishPending)
1072
+ return;
1073
+ if (this.isRevoked())
1074
+ return;
1075
+ if (!entry)
1076
+ return;
1077
+ const actorInfo = Dacument.requireActorInfo();
1078
+ const stamp = this.clock.next();
1079
+ const payload = {
1080
+ iss: this.actorId,
1081
+ sub: this.docId,
1082
+ iat: nowSeconds(),
1083
+ stamp,
1084
+ kind: "acl.set",
1085
+ schema: this.schemaId,
1086
+ patch: {
1087
+ id: uuidv7(),
1088
+ target: this.actorId,
1089
+ role: entry.role,
1090
+ publicKeyJwk: actorInfo.publicKeyJwk,
1091
+ },
1092
+ };
1093
+ this.actorKeyPublishPending = true;
1094
+ this.queueActorOp(payload, () => {
1095
+ this.actorKeyPublishPending = false;
1096
+ });
1097
+ }
718
1098
  ack() {
1099
+ this.assertNotReset();
719
1100
  const stamp = this.clock.next();
720
- const role = this.aclLog.roleAt(this.actorId, stamp);
1101
+ const role = this.roleAt(this.actorId, stamp);
721
1102
  if (role === "revoked")
722
1103
  throw new Error("Dacument: revoked actors cannot acknowledge");
723
1104
  const seen = this.clock.current;
@@ -737,6 +1118,8 @@ export class Dacument {
737
1118
  return;
738
1119
  if (this.currentRole === "revoked")
739
1120
  return;
1121
+ if (this.resetState)
1122
+ return;
740
1123
  this.ackScheduled = true;
741
1124
  queueMicrotask(() => {
742
1125
  this.ackScheduled = false;
@@ -765,6 +1148,8 @@ export class Dacument {
765
1148
  return barrier;
766
1149
  }
767
1150
  maybeGc() {
1151
+ if (this.resetState)
1152
+ return;
768
1153
  const barrier = this.computeGcBarrier();
769
1154
  if (!barrier)
770
1155
  return;
@@ -891,8 +1276,9 @@ export class Dacument {
891
1276
  throw new Error(`Dacument: invalid value for '${field}'`);
892
1277
  if (schema.regex && typeof value === "string" && !schema.regex.test(value))
893
1278
  throw new Error(`Dacument: '${field}' failed regex`);
1279
+ this.assertNotReset();
894
1280
  const stamp = this.clock.next();
895
- const role = this.aclLog.roleAt(this.actorId, stamp);
1281
+ const role = this.roleAt(this.actorId, stamp);
896
1282
  if (!this.canWriteField(role))
897
1283
  throw new Error(`Dacument: role '${role}' cannot write '${field}'`);
898
1284
  this.queueLocalOp({
@@ -1071,7 +1457,7 @@ export class Dacument {
1071
1457
  insertAt(index, value) {
1072
1458
  doc.assertValueType(field, value);
1073
1459
  const stamp = doc.clock.next();
1074
- const role = doc.aclLog.roleAt(doc.actorId, stamp);
1460
+ const role = doc.roleAt(doc.actorId, stamp);
1075
1461
  doc.assertWritable(field, role);
1076
1462
  const shadow = doc.shadowFor(field, state);
1077
1463
  const { patches, result } = doc.capturePatches((listener) => shadow.onChange(listener), () => shadow.insertAt(index, value));
@@ -1091,7 +1477,7 @@ export class Dacument {
1091
1477
  },
1092
1478
  deleteAt(index) {
1093
1479
  const stamp = doc.clock.next();
1094
- const role = doc.aclLog.roleAt(doc.actorId, stamp);
1480
+ const role = doc.roleAt(doc.actorId, stamp);
1095
1481
  doc.assertWritable(field, role);
1096
1482
  const shadow = doc.shadowFor(field, state);
1097
1483
  const { patches, result } = doc.capturePatches((listener) => shadow.onChange(listener), () => shadow.deleteAt(index));
@@ -1299,7 +1685,7 @@ export class Dacument {
1299
1685
  commitArrayMutation(field, mutate) {
1300
1686
  const state = this.fields.get(field);
1301
1687
  const stamp = this.clock.next();
1302
- const role = this.aclLog.roleAt(this.actorId, stamp);
1688
+ const role = this.roleAt(this.actorId, stamp);
1303
1689
  this.assertWritable(field, role);
1304
1690
  const shadow = this.shadowFor(field, state);
1305
1691
  const { patches, result } = this.capturePatches((listener) => shadow.onChange(listener), () => mutate(shadow));
@@ -1320,7 +1706,7 @@ export class Dacument {
1320
1706
  commitSetMutation(field, mutate) {
1321
1707
  const state = this.fields.get(field);
1322
1708
  const stamp = this.clock.next();
1323
- const role = this.aclLog.roleAt(this.actorId, stamp);
1709
+ const role = this.roleAt(this.actorId, stamp);
1324
1710
  this.assertWritable(field, role);
1325
1711
  const shadow = this.shadowFor(field, state);
1326
1712
  const { patches, result } = this.capturePatches((listener) => shadow.onChange(listener), () => mutate(shadow));
@@ -1341,7 +1727,7 @@ export class Dacument {
1341
1727
  commitMapMutation(field, mutate) {
1342
1728
  const state = this.fields.get(field);
1343
1729
  const stamp = this.clock.next();
1344
- const role = this.aclLog.roleAt(this.actorId, stamp);
1730
+ const role = this.roleAt(this.actorId, stamp);
1345
1731
  this.assertWritable(field, role);
1346
1732
  const shadow = this.shadowFor(field, state);
1347
1733
  const { patches, result } = this.capturePatches((listener) => shadow.onChange(listener), () => mutate(shadow));
@@ -1362,7 +1748,7 @@ export class Dacument {
1362
1748
  commitRecordMutation(field, mutate) {
1363
1749
  const state = this.fields.get(field);
1364
1750
  const stamp = this.clock.next();
1365
- const role = this.aclLog.roleAt(this.actorId, stamp);
1751
+ const role = this.roleAt(this.actorId, stamp);
1366
1752
  this.assertWritable(field, role);
1367
1753
  const shadow = this.shadowFor(field, state);
1368
1754
  const { patches, result } = this.capturePatches((listener) => shadow.onChange(listener), () => mutate(shadow));
@@ -1393,6 +1779,7 @@ export class Dacument {
1393
1779
  return { patches, result };
1394
1780
  }
1395
1781
  queueLocalOp(payload, role) {
1782
+ this.assertNotReset();
1396
1783
  if (payload.kind === "ack") {
1397
1784
  const header = { alg: "none", typ: TOKEN_TYP };
1398
1785
  const token = encodeToken(header, payload);
@@ -1405,14 +1792,53 @@ export class Dacument {
1405
1792
  throw new Error("Dacument: missing role private key");
1406
1793
  const header = { alg: "ES256", typ: TOKEN_TYP, kid: `${payload.iss}:${role}` };
1407
1794
  const promise = signToken(this.roleKey, header, payload)
1408
- .then((token) => {
1409
- const op = { token };
1795
+ .then(async (token) => {
1796
+ const actorSig = await Dacument.signActorToken(token);
1797
+ const op = { token, actorSig };
1410
1798
  this.emitEvent("change", { type: "change", ops: [op] });
1411
1799
  })
1412
1800
  .catch((error) => this.emitError(error instanceof Error ? error : new Error(String(error))));
1413
1801
  this.pending.add(promise);
1414
1802
  promise.finally(() => this.pending.delete(promise));
1415
1803
  }
1804
+ queueActorOp(payload, onError) {
1805
+ this.assertNotReset();
1806
+ const actorInfo = Dacument.requireActorInfo();
1807
+ const header = {
1808
+ alg: "ES256",
1809
+ typ: TOKEN_TYP,
1810
+ kid: `${payload.iss}:actor`,
1811
+ };
1812
+ const promise = signToken(actorInfo.privateKeyJwk, header, payload)
1813
+ .then(async (token) => {
1814
+ const actorSig = await Dacument.signActorToken(token);
1815
+ const op = { token, actorSig };
1816
+ this.emitEvent("change", { type: "change", ops: [op] });
1817
+ })
1818
+ .catch((error) => {
1819
+ onError?.();
1820
+ this.emitError(error instanceof Error ? error : new Error(String(error)));
1821
+ });
1822
+ this.pending.add(promise);
1823
+ promise.finally(() => this.pending.delete(promise));
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
+ }
1416
1842
  applyRemotePayload(payload, signerRole) {
1417
1843
  this.clock.observe(payload.stamp);
1418
1844
  if (payload.kind === "ack") {
@@ -1421,11 +1847,15 @@ export class Dacument {
1421
1847
  this.ackByActor.set(payload.iss, payload.patch.seen);
1422
1848
  return true;
1423
1849
  }
1424
- if (!signerRole)
1425
- return false;
1426
1850
  if (payload.kind === "acl.set") {
1851
+ if (!signerRole)
1852
+ return false;
1853
+ if (signerRole === "actor")
1854
+ return this.applyAclPayload(payload, null, { skipAuth: true });
1427
1855
  return this.applyAclPayload(payload, signerRole);
1428
1856
  }
1857
+ if (!signerRole || signerRole === "actor")
1858
+ return false;
1429
1859
  if (!payload.field)
1430
1860
  return false;
1431
1861
  const state = this.fields.get(payload.field);
@@ -1444,18 +1874,23 @@ export class Dacument {
1444
1874
  return false;
1445
1875
  }
1446
1876
  }
1447
- applyAclPayload(payload, signerRole) {
1877
+ applyAclPayload(payload, signerRole, options) {
1448
1878
  if (!isAclPatch(payload.patch))
1449
1879
  return false;
1450
1880
  const patch = payload.patch;
1451
- if (!this.canWriteAclTarget(signerRole, patch.role, patch.target, payload.stamp))
1452
- return false;
1881
+ if (!options?.skipAuth) {
1882
+ if (!signerRole)
1883
+ return false;
1884
+ if (!this.canWriteAclTarget(signerRole, patch.role, patch.target, payload.stamp))
1885
+ return false;
1886
+ }
1453
1887
  const assignment = {
1454
1888
  id: patch.id,
1455
1889
  actorId: patch.target,
1456
1890
  role: patch.role,
1457
1891
  stamp: payload.stamp,
1458
1892
  by: payload.iss,
1893
+ publicKeyJwk: patch.publicKeyJwk,
1459
1894
  };
1460
1895
  const accepted = this.aclLog.merge(assignment);
1461
1896
  if (accepted.length)
@@ -1869,8 +2304,9 @@ export class Dacument {
1869
2304
  return count;
1870
2305
  }
1871
2306
  setRole(actorId, role) {
2307
+ this.assertNotReset();
1872
2308
  const stamp = this.clock.next();
1873
- const signerRole = this.aclLog.roleAt(this.actorId, stamp);
2309
+ const signerRole = this.roleAt(this.actorId, stamp);
1874
2310
  if (!this.canWriteAclTarget(signerRole, role, actorId, stamp))
1875
2311
  throw new Error(`Dacument: role '${signerRole}' cannot grant '${role}'`);
1876
2312
  const assignmentId = uuidv7();
@@ -1923,6 +2359,62 @@ export class Dacument {
1923
2359
  return this.recordValue(crdt);
1924
2360
  }
1925
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
+ }
1926
2418
  emitEvent(type, event) {
1927
2419
  const listeners = this.eventListeners.get(type);
1928
2420
  if (!listeners)
@@ -1946,6 +2438,16 @@ export class Dacument {
1946
2438
  stamp,
1947
2439
  });
1948
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
+ }
1949
2451
  emitError(error) {
1950
2452
  this.emitEvent("error", { type: "error", error });
1951
2453
  }
@@ -1963,13 +2465,14 @@ export class Dacument {
1963
2465
  if (!this.canWriteAcl(role, targetRole))
1964
2466
  return false;
1965
2467
  if (role === "manager") {
1966
- const targetRoleAt = this.aclLog.roleAt(targetActorId, stamp);
2468
+ const targetRoleAt = this.roleAt(targetActorId, stamp);
1967
2469
  if (targetRoleAt === "owner")
1968
2470
  return false;
1969
2471
  }
1970
2472
  return true;
1971
2473
  }
1972
2474
  assertWritable(field, role) {
2475
+ this.assertNotReset();
1973
2476
  if (!this.canWriteField(role))
1974
2477
  throw new Error(`Dacument: role '${role}' cannot write '${field}'`);
1975
2478
  }