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.
- package/README.md +50 -5
- package/dist/Dacument/acl.d.ts +1 -0
- package/dist/Dacument/acl.js +13 -0
- package/dist/Dacument/class.d.ts +42 -4
- package/dist/Dacument/class.js +577 -74
- package/dist/Dacument/crypto.d.ts +3 -0
- package/dist/Dacument/crypto.js +25 -1
- package/dist/Dacument/types.d.ts +43 -1
- package/package.json +2 -2
package/dist/Dacument/class.js
CHANGED
|
@@ -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
|
|
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
|
|
131
|
-
static
|
|
132
|
-
|
|
170
|
+
static actorInfo;
|
|
171
|
+
static actorSigner;
|
|
172
|
+
static async setActorInfo(info) {
|
|
173
|
+
if (Dacument.actorInfo)
|
|
133
174
|
return;
|
|
134
|
-
if (!Dacument.isValidActorId(
|
|
135
|
-
throw new Error("Dacument.
|
|
136
|
-
Dacument.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
553
|
+
getRole: (actorId) => this.currentRoleFor(actorId),
|
|
453
554
|
knownActors: () => this.aclLog.knownActors(),
|
|
454
555
|
snapshot: () => this.aclLog.snapshot(),
|
|
455
556
|
};
|
|
456
|
-
this.currentRole = this.
|
|
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
|
|
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
|
|
573
|
-
if (!
|
|
825
|
+
const signerKind = parseSignerKind(decoded.header.kid, payload.iss);
|
|
826
|
+
if (!signerKind) {
|
|
574
827
|
rejected++;
|
|
575
828
|
continue;
|
|
576
829
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
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
|
-
|
|
661
|
-
|
|
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
|
-
|
|
665
|
-
|
|
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.
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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.
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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 (!
|
|
1452
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|