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.
- package/README.md +51 -20
- package/dist/CRArray/class.d.ts +3 -0
- package/dist/CRArray/class.js +147 -30
- package/dist/CRText/class.d.ts +4 -1
- package/dist/CRText/class.js +70 -17
- package/dist/Dacument/class.d.ts +30 -3
- package/dist/Dacument/class.js +362 -92
- package/dist/Dacument/types.d.ts +24 -1
- package/dist/Dacument/types.js +3 -1
- package/package.json +3 -3
package/dist/Dacument/class.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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.
|
|
589
|
+
getRole: (actorId) => this.currentRoleFor(actorId),
|
|
521
590
|
knownActors: () => this.aclLog.knownActors(),
|
|
522
591
|
snapshot: () => this.aclLog.snapshot(),
|
|
523
592
|
};
|
|
524
|
-
this.currentRole = this.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
732
|
-
|
|
850
|
+
const signerKind = parseSignerKind(decoded.header.kid, payload.iss);
|
|
851
|
+
if (!signerKind) {
|
|
852
|
+
rejected++;
|
|
853
|
+
continue;
|
|
733
854
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
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.
|
|
880
|
-
const
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 !==
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
958
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
},
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
1679
|
-
const
|
|
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,
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
}
|