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