dacument 1.0.1 → 1.1.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 +28 -5
- package/dist/Dacument/acl.d.ts +1 -0
- package/dist/Dacument/acl.js +13 -0
- package/dist/Dacument/class.d.ts +18 -4
- package/dist/Dacument/class.js +345 -46
- package/dist/Dacument/crypto.d.ts +3 -0
- package/dist/Dacument/crypto.js +25 -1
- package/dist/Dacument/types.d.ts +23 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -19,10 +19,16 @@ yarn add dacument
|
|
|
19
19
|
|
|
20
20
|
```ts
|
|
21
21
|
import { generateNonce } from "bytecodec";
|
|
22
|
+
import { generateSignPair } from "zeyra";
|
|
22
23
|
import { Dacument } from "dacument";
|
|
23
24
|
|
|
24
25
|
const actorId = generateNonce(); // 256-bit base64url id
|
|
25
|
-
|
|
26
|
+
const actorKeys = await generateSignPair();
|
|
27
|
+
await Dacument.setActorInfo({
|
|
28
|
+
id: actorId,
|
|
29
|
+
privateKeyJwk: actorKeys.signingJwk,
|
|
30
|
+
publicKeyJwk: actorKeys.verificationJwk,
|
|
31
|
+
});
|
|
26
32
|
|
|
27
33
|
const schema = Dacument.schema({
|
|
28
34
|
title: Dacument.register({ jsType: "string", regex: /^[a-z ]+$/i }),
|
|
@@ -93,9 +99,11 @@ doc.acl.setRole("user-viewer", "viewer");
|
|
|
93
99
|
await doc.flush();
|
|
94
100
|
```
|
|
95
101
|
|
|
96
|
-
Before any schema/load/create, call `Dacument.
|
|
97
|
-
The actor id must be a 256-bit base64url string (e.g.
|
|
98
|
-
|
|
102
|
+
Before any schema/load/create, call `await Dacument.setActorInfo(...)` once per
|
|
103
|
+
process. The actor id must be a 256-bit base64url string (e.g.
|
|
104
|
+
`bytecodec` libarys `generateNonce()`), and the actor key pair must be ES256 (P-256).
|
|
105
|
+
Subsequent calls are ignored. On first merge, Dacument auto-attaches the
|
|
106
|
+
actor's `publicKeyJwk` to its own ACL entry (if missing).
|
|
99
107
|
|
|
100
108
|
Each actor signs with the role key they were given (owner/manager/editor). Load
|
|
101
109
|
with the highest role key you have; viewers load without a key.
|
|
@@ -123,7 +131,12 @@ without snapshotting.
|
|
|
123
131
|
To add a new replica, share a snapshot and load it:
|
|
124
132
|
|
|
125
133
|
```ts
|
|
126
|
-
|
|
134
|
+
const bobKeys = await generateSignPair();
|
|
135
|
+
await Dacument.setActorInfo({
|
|
136
|
+
id: bobId,
|
|
137
|
+
privateKeyJwk: bobKeys.signingJwk,
|
|
138
|
+
publicKeyJwk: bobKeys.verificationJwk,
|
|
139
|
+
});
|
|
127
140
|
const bob = await Dacument.load({
|
|
128
141
|
schema,
|
|
129
142
|
roleKey: bobKey.privateKey,
|
|
@@ -139,10 +152,20 @@ Snapshots do not include schema or schema ids; callers must supply the schema on
|
|
|
139
152
|
- `doc.addEventListener("merge", handler)` emits `{ actor, target, method, data }`.
|
|
140
153
|
- `doc.addEventListener("error", handler)` emits signing/verification errors.
|
|
141
154
|
- `doc.addEventListener("revoked", handler)` fires when the current actor is revoked.
|
|
155
|
+
- `doc.selfRevoke()` emits a signed ACL op that revokes the current actor.
|
|
142
156
|
- `await doc.flush()` waits for pending signatures so all local ops are emitted.
|
|
143
157
|
- `doc.snapshot()` returns a loadable op log (`{ docId, roleKeys, ops }`).
|
|
158
|
+
- `await doc.verifyActorIntegrity(...)` verifies per-actor signatures on demand.
|
|
144
159
|
- Revoked actors cannot snapshot; reads are masked to initial values.
|
|
145
160
|
|
|
161
|
+
## Actor identity (cold path)
|
|
162
|
+
|
|
163
|
+
Every op may include an `actorSig` (detached ES256 signature over the op token).
|
|
164
|
+
Merges ignore `actorSig` by default to keep the hot path fast. When you need
|
|
165
|
+
attribution or forensic checks, call `verifyActorIntegrity()` with a token,
|
|
166
|
+
ops list, or snapshot. It verifies `actorSig` against the actor's `publicKeyJwk`
|
|
167
|
+
from the ACL at the op stamp and returns a summary plus failures.
|
|
168
|
+
|
|
146
169
|
## Garbage collection
|
|
147
170
|
|
|
148
171
|
Dacument tracks per-actor `ack` ops and compacts tombstones once all non-revoked
|
package/dist/Dacument/acl.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare class AclLog {
|
|
|
11
11
|
roleAt(actorId: string, stamp: AclAssignment["stamp"]): Role;
|
|
12
12
|
currentRole(actorId: string): Role;
|
|
13
13
|
currentEntry(actorId: string): AclAssignment | null;
|
|
14
|
+
publicKeyAt(actorId: string, stamp: AclAssignment["stamp"]): JsonWebKey | null;
|
|
14
15
|
knownActors(): string[];
|
|
15
16
|
private insert;
|
|
16
17
|
}
|
package/dist/Dacument/acl.js
CHANGED
|
@@ -55,6 +55,19 @@ export class AclLog {
|
|
|
55
55
|
currentEntry(actorId) {
|
|
56
56
|
return this.currentByActor.get(actorId) ?? null;
|
|
57
57
|
}
|
|
58
|
+
publicKeyAt(actorId, stamp) {
|
|
59
|
+
const list = this.nodesByActor.get(actorId);
|
|
60
|
+
if (!list || list.length === 0)
|
|
61
|
+
return null;
|
|
62
|
+
for (let index = list.length - 1; index >= 0; index--) {
|
|
63
|
+
const entry = list[index];
|
|
64
|
+
if (compareHLC(entry.stamp, stamp) > 0)
|
|
65
|
+
continue;
|
|
66
|
+
if (entry.publicKeyJwk)
|
|
67
|
+
return entry.publicKeyJwk;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
58
71
|
knownActors() {
|
|
59
72
|
return [...this.currentByActor.keys()];
|
|
60
73
|
}
|
package/dist/Dacument/class.d.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { type AclAssignment, type DacumentEventMap, type DocFieldAccess, type DocSnapshot, type RoleKeys, type RolePublicKeys, type SchemaDefinition, type SchemaId, type SignedOp, type Role, array, map, record, register, set, text } from "./types.js";
|
|
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
2
|
export declare class Dacument<S extends SchemaDefinition> {
|
|
3
|
-
private static
|
|
4
|
-
static
|
|
5
|
-
|
|
3
|
+
private static actorInfo?;
|
|
4
|
+
private static actorSigner?;
|
|
5
|
+
static setActorInfo(info: ActorInfo): Promise<void>;
|
|
6
|
+
private static requireActorInfo;
|
|
7
|
+
private static requireActorSigner;
|
|
8
|
+
private static signActorToken;
|
|
6
9
|
private static isValidActorId;
|
|
10
|
+
private static assertActorKeyJwk;
|
|
11
|
+
private static assertActorPrivateKey;
|
|
12
|
+
private static assertActorPublicKey;
|
|
7
13
|
static schema: <Schema extends SchemaDefinition>(schema: Schema) => Schema;
|
|
8
14
|
static register: typeof register;
|
|
9
15
|
static text: typeof text;
|
|
@@ -38,6 +44,8 @@ export declare class Dacument<S extends SchemaDefinition> {
|
|
|
38
44
|
private readonly opLog;
|
|
39
45
|
private readonly opTokens;
|
|
40
46
|
private readonly verifiedOps;
|
|
47
|
+
private readonly opIndexByToken;
|
|
48
|
+
private readonly actorSigByToken;
|
|
41
49
|
private readonly appliedTokens;
|
|
42
50
|
private currentRole;
|
|
43
51
|
private readonly revokedCrdtByField;
|
|
@@ -49,8 +57,10 @@ export declare class Dacument<S extends SchemaDefinition> {
|
|
|
49
57
|
private readonly ackByActor;
|
|
50
58
|
private suppressMerge;
|
|
51
59
|
private ackScheduled;
|
|
60
|
+
private actorKeyPublishPending;
|
|
52
61
|
private lastGcBarrier;
|
|
53
62
|
private snapshotFieldValues;
|
|
63
|
+
private recordActorSig;
|
|
54
64
|
readonly acl: {
|
|
55
65
|
setRole: (actorId: string, role: Role) => void;
|
|
56
66
|
getRole: (actorId: string) => Role;
|
|
@@ -68,11 +78,14 @@ export declare class Dacument<S extends SchemaDefinition> {
|
|
|
68
78
|
removeEventListener<K extends keyof DacumentEventMap>(type: K, listener: (event: DacumentEventMap[K]) => void): void;
|
|
69
79
|
flush(): Promise<void>;
|
|
70
80
|
snapshot(): DocSnapshot;
|
|
81
|
+
selfRevoke(): void;
|
|
82
|
+
verifyActorIntegrity(options?: VerifyActorIntegrityOptions): Promise<VerificationResult>;
|
|
71
83
|
merge(input: SignedOp | SignedOp[] | string | string[]): Promise<{
|
|
72
84
|
accepted: SignedOp[];
|
|
73
85
|
rejected: number;
|
|
74
86
|
}>;
|
|
75
87
|
private rebuildFromVerified;
|
|
88
|
+
private maybePublishActorKey;
|
|
76
89
|
private ack;
|
|
77
90
|
private scheduleAck;
|
|
78
91
|
private computeGcBarrier;
|
|
@@ -102,6 +115,7 @@ export declare class Dacument<S extends SchemaDefinition> {
|
|
|
102
115
|
private commitRecordMutation;
|
|
103
116
|
private capturePatches;
|
|
104
117
|
private queueLocalOp;
|
|
118
|
+
private queueActorOp;
|
|
105
119
|
private applyRemotePayload;
|
|
106
120
|
private applyAclPayload;
|
|
107
121
|
private applyRegisterPayload;
|
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() {
|
|
@@ -35,6 +35,15 @@ function stableKey(value) {
|
|
|
35
35
|
}
|
|
36
36
|
return JSON.stringify(value);
|
|
37
37
|
}
|
|
38
|
+
function normalizeJwk(jwk) {
|
|
39
|
+
const entries = Object.entries(jwk).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
40
|
+
return JSON.stringify(Object.fromEntries(entries));
|
|
41
|
+
}
|
|
42
|
+
function jwkEquals(left, right) {
|
|
43
|
+
if (!left || !right)
|
|
44
|
+
return false;
|
|
45
|
+
return normalizeJwk(left) === normalizeJwk(right);
|
|
46
|
+
}
|
|
38
47
|
function isDagNode(node) {
|
|
39
48
|
if (!isObject(node))
|
|
40
49
|
return false;
|
|
@@ -55,6 +64,13 @@ function isAclPatch(value) {
|
|
|
55
64
|
return false;
|
|
56
65
|
if (typeof value.role !== "string")
|
|
57
66
|
return false;
|
|
67
|
+
if ("publicKeyJwk" in value && value.publicKeyJwk !== undefined) {
|
|
68
|
+
if (!isObject(value.publicKeyJwk))
|
|
69
|
+
return false;
|
|
70
|
+
const jwk = value.publicKeyJwk;
|
|
71
|
+
if (jwk.kty && jwk.kty !== "EC")
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
58
74
|
return true;
|
|
59
75
|
}
|
|
60
76
|
function isAckPatch(value) {
|
|
@@ -99,7 +115,7 @@ function createEmptyField(crdt) {
|
|
|
99
115
|
function roleNeedsKey(role) {
|
|
100
116
|
return role === "owner" || role === "manager" || role === "editor";
|
|
101
117
|
}
|
|
102
|
-
function
|
|
118
|
+
function parseSignerKind(kid, issuer) {
|
|
103
119
|
if (!kid)
|
|
104
120
|
return null;
|
|
105
121
|
const [kidIssuer, role] = kid.split(":");
|
|
@@ -107,6 +123,8 @@ function parseSignerRole(kid, issuer) {
|
|
|
107
123
|
return null;
|
|
108
124
|
if (role === "owner" || role === "manager" || role === "editor")
|
|
109
125
|
return role;
|
|
126
|
+
if (role === "actor")
|
|
127
|
+
return "actor";
|
|
110
128
|
return null;
|
|
111
129
|
}
|
|
112
130
|
async function generateRoleKeys() {
|
|
@@ -127,18 +145,33 @@ function toPublicRoleKeys(roleKeys) {
|
|
|
127
145
|
};
|
|
128
146
|
}
|
|
129
147
|
export class Dacument {
|
|
130
|
-
static
|
|
131
|
-
static
|
|
132
|
-
|
|
148
|
+
static actorInfo;
|
|
149
|
+
static actorSigner;
|
|
150
|
+
static async setActorInfo(info) {
|
|
151
|
+
if (Dacument.actorInfo)
|
|
133
152
|
return;
|
|
134
|
-
if (!Dacument.isValidActorId(
|
|
135
|
-
throw new Error("Dacument.
|
|
136
|
-
Dacument.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
153
|
+
if (!Dacument.isValidActorId(info.id))
|
|
154
|
+
throw new Error("Dacument.setActorInfo: id must be 256-bit base64url");
|
|
155
|
+
Dacument.assertActorPrivateKey(info.privateKeyJwk);
|
|
156
|
+
Dacument.assertActorPublicKey(info.publicKeyJwk);
|
|
157
|
+
await validateActorKeyPair(info.privateKeyJwk, info.publicKeyJwk);
|
|
158
|
+
Dacument.actorInfo = info;
|
|
159
|
+
Dacument.actorSigner = new SigningAgent(info.privateKeyJwk);
|
|
160
|
+
}
|
|
161
|
+
static requireActorInfo() {
|
|
162
|
+
if (!Dacument.actorInfo)
|
|
163
|
+
throw new Error("Dacument: actor info not set; call Dacument.setActorInfo()");
|
|
164
|
+
return Dacument.actorInfo;
|
|
165
|
+
}
|
|
166
|
+
static requireActorSigner() {
|
|
167
|
+
if (!Dacument.actorSigner)
|
|
168
|
+
throw new Error("Dacument: actor info not set; call Dacument.setActorInfo()");
|
|
169
|
+
return Dacument.actorSigner;
|
|
170
|
+
}
|
|
171
|
+
static async signActorToken(token) {
|
|
172
|
+
const signer = Dacument.requireActorSigner();
|
|
173
|
+
const signature = await signer.sign(Bytes.fromString(token));
|
|
174
|
+
return Bytes.toBase64UrlString(signature);
|
|
142
175
|
}
|
|
143
176
|
static isValidActorId(actorId) {
|
|
144
177
|
if (typeof actorId !== "string")
|
|
@@ -151,8 +184,26 @@ export class Dacument {
|
|
|
151
184
|
return false;
|
|
152
185
|
}
|
|
153
186
|
}
|
|
187
|
+
static assertActorKeyJwk(jwk, label) {
|
|
188
|
+
if (!jwk || typeof jwk !== "object")
|
|
189
|
+
throw new Error(`Dacument.setActorInfo: ${label} must be a JWK object`);
|
|
190
|
+
if (jwk.kty !== "EC")
|
|
191
|
+
throw new Error(`Dacument.setActorInfo: ${label} must be EC (P-256)`);
|
|
192
|
+
if (jwk.crv && jwk.crv !== "P-256")
|
|
193
|
+
throw new Error(`Dacument.setActorInfo: ${label} must use P-256`);
|
|
194
|
+
if (jwk.alg && jwk.alg !== "ES256")
|
|
195
|
+
throw new Error(`Dacument.setActorInfo: ${label} must use ES256`);
|
|
196
|
+
}
|
|
197
|
+
static assertActorPrivateKey(jwk) {
|
|
198
|
+
Dacument.assertActorKeyJwk(jwk, "privateKeyJwk");
|
|
199
|
+
if (!jwk.d)
|
|
200
|
+
throw new Error("Dacument.setActorInfo: privateKeyJwk must include 'd'");
|
|
201
|
+
}
|
|
202
|
+
static assertActorPublicKey(jwk) {
|
|
203
|
+
Dacument.assertActorKeyJwk(jwk, "publicKeyJwk");
|
|
204
|
+
}
|
|
154
205
|
static schema = (schema) => {
|
|
155
|
-
Dacument.
|
|
206
|
+
Dacument.requireActorInfo();
|
|
156
207
|
return schema;
|
|
157
208
|
};
|
|
158
209
|
static register = register;
|
|
@@ -173,7 +224,8 @@ export class Dacument {
|
|
|
173
224
|
return Bytes.toBase64UrlString(new Uint8Array(digest));
|
|
174
225
|
}
|
|
175
226
|
static async create(params) {
|
|
176
|
-
const
|
|
227
|
+
const ownerInfo = Dacument.requireActorInfo();
|
|
228
|
+
const ownerId = ownerInfo.id;
|
|
177
229
|
const docId = params.docId ?? generateNonce();
|
|
178
230
|
const schemaId = await Dacument.computeSchemaId(params.schema);
|
|
179
231
|
const roleKeys = await generateRoleKeys();
|
|
@@ -198,7 +250,8 @@ export class Dacument {
|
|
|
198
250
|
};
|
|
199
251
|
const sign = async (payload) => {
|
|
200
252
|
const token = await signToken(roleKeys.owner.privateKey, header, payload);
|
|
201
|
-
|
|
253
|
+
const actorSig = await Dacument.signActorToken(token);
|
|
254
|
+
ops.push({ token, actorSig });
|
|
202
255
|
};
|
|
203
256
|
await sign({
|
|
204
257
|
iss: ownerId,
|
|
@@ -211,6 +264,7 @@ export class Dacument {
|
|
|
211
264
|
id: uuidv7(),
|
|
212
265
|
target: ownerId,
|
|
213
266
|
role: "owner",
|
|
267
|
+
publicKeyJwk: ownerInfo.publicKeyJwk,
|
|
214
268
|
},
|
|
215
269
|
});
|
|
216
270
|
for (const [field, schema] of Object.entries(params.schema)) {
|
|
@@ -390,7 +444,7 @@ export class Dacument {
|
|
|
390
444
|
return { docId, schemaId, roleKeys, snapshot };
|
|
391
445
|
}
|
|
392
446
|
static async load(params) {
|
|
393
|
-
const actorId = Dacument.
|
|
447
|
+
const actorId = Dacument.requireActorInfo().id;
|
|
394
448
|
const schemaId = await Dacument.computeSchemaId(params.schema);
|
|
395
449
|
const doc = new Dacument({
|
|
396
450
|
schema: params.schema,
|
|
@@ -414,6 +468,8 @@ export class Dacument {
|
|
|
414
468
|
opLog = [];
|
|
415
469
|
opTokens = new Set();
|
|
416
470
|
verifiedOps = new Map();
|
|
471
|
+
opIndexByToken = new Map();
|
|
472
|
+
actorSigByToken = new Map();
|
|
417
473
|
appliedTokens = new Set();
|
|
418
474
|
currentRole;
|
|
419
475
|
revokedCrdtByField = new Map();
|
|
@@ -425,6 +481,7 @@ export class Dacument {
|
|
|
425
481
|
ackByActor = new Map();
|
|
426
482
|
suppressMerge = false;
|
|
427
483
|
ackScheduled = false;
|
|
484
|
+
actorKeyPublishPending = false;
|
|
428
485
|
lastGcBarrier = null;
|
|
429
486
|
snapshotFieldValues() {
|
|
430
487
|
const values = new Map();
|
|
@@ -432,9 +489,20 @@ export class Dacument {
|
|
|
432
489
|
values.set(key, this.fieldValue(key));
|
|
433
490
|
return values;
|
|
434
491
|
}
|
|
492
|
+
recordActorSig(token, actorSig) {
|
|
493
|
+
if (!actorSig || this.actorSigByToken.has(token))
|
|
494
|
+
return;
|
|
495
|
+
this.actorSigByToken.set(token, actorSig);
|
|
496
|
+
const index = this.opIndexByToken.get(token);
|
|
497
|
+
if (index === undefined)
|
|
498
|
+
return;
|
|
499
|
+
const entry = this.opLog[index];
|
|
500
|
+
if (!entry.actorSig)
|
|
501
|
+
entry.actorSig = actorSig;
|
|
502
|
+
}
|
|
435
503
|
acl;
|
|
436
504
|
constructor(params) {
|
|
437
|
-
const actorId = Dacument.
|
|
505
|
+
const actorId = Dacument.requireActorInfo().id;
|
|
438
506
|
this.schema = params.schema;
|
|
439
507
|
this.schemaId = params.schemaId;
|
|
440
508
|
this.docId = params.docId;
|
|
@@ -522,10 +590,104 @@ export class Dacument {
|
|
|
522
590
|
snapshot() {
|
|
523
591
|
if (this.isRevoked())
|
|
524
592
|
throw new Error("Dacument: revoked actors cannot snapshot");
|
|
593
|
+
const ops = this.opLog.map((op) => {
|
|
594
|
+
const actorSig = this.actorSigByToken.get(op.token);
|
|
595
|
+
return actorSig ? { token: op.token, actorSig } : { token: op.token };
|
|
596
|
+
});
|
|
525
597
|
return {
|
|
526
598
|
docId: this.docId,
|
|
527
599
|
roleKeys: this.roleKeys,
|
|
528
|
-
ops
|
|
600
|
+
ops,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
selfRevoke() {
|
|
604
|
+
const stamp = this.clock.next();
|
|
605
|
+
const role = this.aclLog.roleAt(this.actorId, stamp);
|
|
606
|
+
if (role === "revoked")
|
|
607
|
+
return;
|
|
608
|
+
const actorInfo = Dacument.requireActorInfo();
|
|
609
|
+
const entry = this.aclLog.currentEntry(this.actorId);
|
|
610
|
+
const patch = {
|
|
611
|
+
id: uuidv7(),
|
|
612
|
+
target: this.actorId,
|
|
613
|
+
role: "revoked",
|
|
614
|
+
};
|
|
615
|
+
if (!entry?.publicKeyJwk)
|
|
616
|
+
patch.publicKeyJwk = actorInfo.publicKeyJwk;
|
|
617
|
+
const payload = {
|
|
618
|
+
iss: this.actorId,
|
|
619
|
+
sub: this.docId,
|
|
620
|
+
iat: nowSeconds(),
|
|
621
|
+
stamp,
|
|
622
|
+
kind: "acl.set",
|
|
623
|
+
schema: this.schemaId,
|
|
624
|
+
patch,
|
|
625
|
+
};
|
|
626
|
+
if (roleNeedsKey(role) && this.roleKey) {
|
|
627
|
+
this.queueLocalOp(payload, role);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
this.queueActorOp(payload);
|
|
631
|
+
}
|
|
632
|
+
async verifyActorIntegrity(options = {}) {
|
|
633
|
+
const input = options.token !== undefined
|
|
634
|
+
? [options.token]
|
|
635
|
+
: options.ops ?? options.snapshot?.ops ?? this.opLog;
|
|
636
|
+
let verified = 0;
|
|
637
|
+
let failed = 0;
|
|
638
|
+
let missing = 0;
|
|
639
|
+
const failures = [];
|
|
640
|
+
for (let index = 0; index < input.length; index++) {
|
|
641
|
+
const item = input[index];
|
|
642
|
+
const token = typeof item === "string" ? item : item.token;
|
|
643
|
+
const actorSig = typeof item === "string"
|
|
644
|
+
? this.actorSigByToken.get(token)
|
|
645
|
+
: item.actorSig ?? this.actorSigByToken.get(token);
|
|
646
|
+
const decoded = decodeToken(token);
|
|
647
|
+
if (!decoded) {
|
|
648
|
+
failed++;
|
|
649
|
+
failures.push({ index, reason: "invalid token" });
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
const payload = decoded.payload;
|
|
653
|
+
if (!this.isValidPayload(payload)) {
|
|
654
|
+
failed++;
|
|
655
|
+
failures.push({ index, reason: "invalid payload" });
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (!actorSig) {
|
|
659
|
+
missing++;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const publicKey = this.aclLog.publicKeyAt(payload.iss, payload.stamp);
|
|
663
|
+
if (!publicKey) {
|
|
664
|
+
missing++;
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
try {
|
|
668
|
+
const ok = await verifyDetached(publicKey, token, actorSig);
|
|
669
|
+
if (!ok) {
|
|
670
|
+
failed++;
|
|
671
|
+
failures.push({ index, reason: "actor signature mismatch" });
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
failed++;
|
|
677
|
+
failures.push({
|
|
678
|
+
index,
|
|
679
|
+
reason: error instanceof Error ? error.message : "actor signature error",
|
|
680
|
+
});
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
verified++;
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
ok: failed === 0,
|
|
687
|
+
verified,
|
|
688
|
+
failed,
|
|
689
|
+
missing,
|
|
690
|
+
failures,
|
|
529
691
|
};
|
|
530
692
|
}
|
|
531
693
|
async merge(input) {
|
|
@@ -538,6 +700,7 @@ export class Dacument {
|
|
|
538
700
|
let diffStamp = null;
|
|
539
701
|
for (const item of tokens) {
|
|
540
702
|
const token = typeof item === "string" ? item : item.token;
|
|
703
|
+
const actorSig = typeof item === "string" ? undefined : item.actorSig;
|
|
541
704
|
const decoded = decodeToken(token);
|
|
542
705
|
if (!decoded) {
|
|
543
706
|
rejected++;
|
|
@@ -569,23 +732,66 @@ export class Dacument {
|
|
|
569
732
|
stored = { payload, signerRole: null };
|
|
570
733
|
}
|
|
571
734
|
else {
|
|
572
|
-
const
|
|
573
|
-
if (!
|
|
735
|
+
const signerKind = parseSignerKind(decoded.header.kid, payload.iss);
|
|
736
|
+
if (!signerKind) {
|
|
574
737
|
rejected++;
|
|
575
738
|
continue;
|
|
576
739
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
740
|
+
if (signerKind === "actor") {
|
|
741
|
+
if (payload.kind !== "acl.set") {
|
|
742
|
+
rejected++;
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
const patch = isAclPatch(payload.patch) ? payload.patch : null;
|
|
746
|
+
if (!patch || patch.target !== payload.iss) {
|
|
747
|
+
rejected++;
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
const wantsSelfRevoke = patch.role === "revoked";
|
|
751
|
+
const wantsKeyAttach = Boolean(patch.publicKeyJwk);
|
|
752
|
+
if (!wantsSelfRevoke && !wantsKeyAttach) {
|
|
753
|
+
rejected++;
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
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
|
+
const publicKey = existingKey ?? patch.publicKeyJwk;
|
|
764
|
+
if (!publicKey) {
|
|
765
|
+
rejected++;
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
const verified = await verifyToken(publicKey, token, TOKEN_TYP);
|
|
769
|
+
if (!verified) {
|
|
770
|
+
rejected++;
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
stored = { payload, signerRole: "actor" };
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
const publicKey = this.roleKeys[signerKind];
|
|
777
|
+
const verified = await verifyToken(publicKey, token, TOKEN_TYP);
|
|
778
|
+
if (!verified) {
|
|
779
|
+
rejected++;
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
stored = { payload, signerRole: signerKind };
|
|
582
783
|
}
|
|
583
|
-
stored = { payload, signerRole };
|
|
584
784
|
}
|
|
585
785
|
this.verifiedOps.set(token, stored);
|
|
586
786
|
if (!this.opTokens.has(token)) {
|
|
587
787
|
this.opTokens.add(token);
|
|
588
|
-
|
|
788
|
+
const opEntry = { token };
|
|
789
|
+
if (typeof actorSig === "string") {
|
|
790
|
+
opEntry.actorSig = actorSig;
|
|
791
|
+
this.actorSigByToken.set(token, actorSig);
|
|
792
|
+
}
|
|
793
|
+
this.opLog.push(opEntry);
|
|
794
|
+
this.opIndexByToken.set(token, this.opLog.length - 1);
|
|
589
795
|
}
|
|
590
796
|
sawNewToken = true;
|
|
591
797
|
if (payload.kind === "acl.set") {
|
|
@@ -595,6 +801,7 @@ export class Dacument {
|
|
|
595
801
|
}
|
|
596
802
|
}
|
|
597
803
|
}
|
|
804
|
+
this.recordActorSig(token, actorSig);
|
|
598
805
|
decodedOps.push({
|
|
599
806
|
token,
|
|
600
807
|
payload: stored.payload,
|
|
@@ -605,7 +812,7 @@ export class Dacument {
|
|
|
605
812
|
let appliedNonAck = false;
|
|
606
813
|
if (sawNewToken) {
|
|
607
814
|
const beforeValues = this.isRevoked() ? undefined : this.snapshotFieldValues();
|
|
608
|
-
const result = this.rebuildFromVerified(new Set(this.appliedTokens), {
|
|
815
|
+
const result = await this.rebuildFromVerified(new Set(this.appliedTokens), {
|
|
609
816
|
beforeValues,
|
|
610
817
|
diffActor: diffActor ?? this.actorId,
|
|
611
818
|
});
|
|
@@ -627,9 +834,10 @@ export class Dacument {
|
|
|
627
834
|
if (appliedNonAck)
|
|
628
835
|
this.scheduleAck();
|
|
629
836
|
this.maybeGc();
|
|
837
|
+
this.maybePublishActorKey();
|
|
630
838
|
return { accepted, rejected };
|
|
631
839
|
}
|
|
632
|
-
rebuildFromVerified(previousApplied, options) {
|
|
840
|
+
async rebuildFromVerified(previousApplied, options) {
|
|
633
841
|
const invalidated = new Set(previousApplied);
|
|
634
842
|
let appliedNonAck = false;
|
|
635
843
|
this.aclLog.reset();
|
|
@@ -658,21 +866,49 @@ export class Dacument {
|
|
|
658
866
|
for (const { token, payload, signerRole } of ops) {
|
|
659
867
|
let allowed = false;
|
|
660
868
|
if (payload.kind === "acl.set") {
|
|
661
|
-
|
|
869
|
+
const patch = isAclPatch(payload.patch) ? payload.patch : null;
|
|
870
|
+
if (!patch)
|
|
662
871
|
continue;
|
|
663
872
|
if (this.aclLog.isEmpty() &&
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
payload.patch.target === payload.iss &&
|
|
873
|
+
patch.role === "owner" &&
|
|
874
|
+
patch.target === payload.iss &&
|
|
667
875
|
signerRole === "owner") {
|
|
668
876
|
allowed = true;
|
|
669
877
|
}
|
|
670
878
|
else {
|
|
671
879
|
const roleAt = this.aclLog.roleAt(payload.iss, payload.stamp);
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
880
|
+
const isSelfRevoke = patch.target === payload.iss && patch.role === "revoked";
|
|
881
|
+
const targetKey = this.aclLog.publicKeyAt(patch.target, payload.stamp);
|
|
882
|
+
const isSelfKeyUpdate = patch.target === payload.iss &&
|
|
883
|
+
patch.publicKeyJwk &&
|
|
884
|
+
patch.role === roleAt &&
|
|
885
|
+
roleAt !== "revoked" &&
|
|
886
|
+
(!targetKey || jwkEquals(targetKey, patch.publicKeyJwk));
|
|
887
|
+
if (patch.publicKeyJwk &&
|
|
888
|
+
targetKey &&
|
|
889
|
+
!jwkEquals(targetKey, patch.publicKeyJwk)) {
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (isSelfRevoke) {
|
|
893
|
+
if (signerRole === "actor") {
|
|
894
|
+
allowed = true;
|
|
895
|
+
}
|
|
896
|
+
else if (signerRole && roleAt === signerRole) {
|
|
897
|
+
allowed = true;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
else if (signerRole === "actor") {
|
|
901
|
+
if (isSelfKeyUpdate)
|
|
902
|
+
allowed = true;
|
|
903
|
+
}
|
|
904
|
+
else if (signerRole && roleAt === signerRole) {
|
|
905
|
+
if (this.canWriteAclTarget(signerRole, patch.role, patch.target, payload.stamp)) {
|
|
906
|
+
allowed = true;
|
|
907
|
+
}
|
|
908
|
+
else if (isSelfKeyUpdate) {
|
|
909
|
+
allowed = true;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
676
912
|
}
|
|
677
913
|
}
|
|
678
914
|
else {
|
|
@@ -715,6 +951,39 @@ export class Dacument {
|
|
|
715
951
|
}
|
|
716
952
|
return { appliedNonAck };
|
|
717
953
|
}
|
|
954
|
+
maybePublishActorKey() {
|
|
955
|
+
const entry = this.aclLog.currentEntry(this.actorId);
|
|
956
|
+
if (entry?.publicKeyJwk) {
|
|
957
|
+
this.actorKeyPublishPending = false;
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (this.actorKeyPublishPending)
|
|
961
|
+
return;
|
|
962
|
+
if (this.isRevoked())
|
|
963
|
+
return;
|
|
964
|
+
if (!entry)
|
|
965
|
+
return;
|
|
966
|
+
const actorInfo = Dacument.requireActorInfo();
|
|
967
|
+
const stamp = this.clock.next();
|
|
968
|
+
const payload = {
|
|
969
|
+
iss: this.actorId,
|
|
970
|
+
sub: this.docId,
|
|
971
|
+
iat: nowSeconds(),
|
|
972
|
+
stamp,
|
|
973
|
+
kind: "acl.set",
|
|
974
|
+
schema: this.schemaId,
|
|
975
|
+
patch: {
|
|
976
|
+
id: uuidv7(),
|
|
977
|
+
target: this.actorId,
|
|
978
|
+
role: entry.role,
|
|
979
|
+
publicKeyJwk: actorInfo.publicKeyJwk,
|
|
980
|
+
},
|
|
981
|
+
};
|
|
982
|
+
this.actorKeyPublishPending = true;
|
|
983
|
+
this.queueActorOp(payload, () => {
|
|
984
|
+
this.actorKeyPublishPending = false;
|
|
985
|
+
});
|
|
986
|
+
}
|
|
718
987
|
ack() {
|
|
719
988
|
const stamp = this.clock.next();
|
|
720
989
|
const role = this.aclLog.roleAt(this.actorId, stamp);
|
|
@@ -1405,14 +1674,35 @@ export class Dacument {
|
|
|
1405
1674
|
throw new Error("Dacument: missing role private key");
|
|
1406
1675
|
const header = { alg: "ES256", typ: TOKEN_TYP, kid: `${payload.iss}:${role}` };
|
|
1407
1676
|
const promise = signToken(this.roleKey, header, payload)
|
|
1408
|
-
.then((token) => {
|
|
1409
|
-
const
|
|
1677
|
+
.then(async (token) => {
|
|
1678
|
+
const actorSig = await Dacument.signActorToken(token);
|
|
1679
|
+
const op = { token, actorSig };
|
|
1410
1680
|
this.emitEvent("change", { type: "change", ops: [op] });
|
|
1411
1681
|
})
|
|
1412
1682
|
.catch((error) => this.emitError(error instanceof Error ? error : new Error(String(error))));
|
|
1413
1683
|
this.pending.add(promise);
|
|
1414
1684
|
promise.finally(() => this.pending.delete(promise));
|
|
1415
1685
|
}
|
|
1686
|
+
queueActorOp(payload, onError) {
|
|
1687
|
+
const actorInfo = Dacument.requireActorInfo();
|
|
1688
|
+
const header = {
|
|
1689
|
+
alg: "ES256",
|
|
1690
|
+
typ: TOKEN_TYP,
|
|
1691
|
+
kid: `${payload.iss}:actor`,
|
|
1692
|
+
};
|
|
1693
|
+
const promise = signToken(actorInfo.privateKeyJwk, header, payload)
|
|
1694
|
+
.then(async (token) => {
|
|
1695
|
+
const actorSig = await Dacument.signActorToken(token);
|
|
1696
|
+
const op = { token, actorSig };
|
|
1697
|
+
this.emitEvent("change", { type: "change", ops: [op] });
|
|
1698
|
+
})
|
|
1699
|
+
.catch((error) => {
|
|
1700
|
+
onError?.();
|
|
1701
|
+
this.emitError(error instanceof Error ? error : new Error(String(error)));
|
|
1702
|
+
});
|
|
1703
|
+
this.pending.add(promise);
|
|
1704
|
+
promise.finally(() => this.pending.delete(promise));
|
|
1705
|
+
}
|
|
1416
1706
|
applyRemotePayload(payload, signerRole) {
|
|
1417
1707
|
this.clock.observe(payload.stamp);
|
|
1418
1708
|
if (payload.kind === "ack") {
|
|
@@ -1421,11 +1711,15 @@ export class Dacument {
|
|
|
1421
1711
|
this.ackByActor.set(payload.iss, payload.patch.seen);
|
|
1422
1712
|
return true;
|
|
1423
1713
|
}
|
|
1424
|
-
if (!signerRole)
|
|
1425
|
-
return false;
|
|
1426
1714
|
if (payload.kind === "acl.set") {
|
|
1715
|
+
if (!signerRole)
|
|
1716
|
+
return false;
|
|
1717
|
+
if (signerRole === "actor")
|
|
1718
|
+
return this.applyAclPayload(payload, null, { skipAuth: true });
|
|
1427
1719
|
return this.applyAclPayload(payload, signerRole);
|
|
1428
1720
|
}
|
|
1721
|
+
if (!signerRole || signerRole === "actor")
|
|
1722
|
+
return false;
|
|
1429
1723
|
if (!payload.field)
|
|
1430
1724
|
return false;
|
|
1431
1725
|
const state = this.fields.get(payload.field);
|
|
@@ -1444,18 +1738,23 @@ export class Dacument {
|
|
|
1444
1738
|
return false;
|
|
1445
1739
|
}
|
|
1446
1740
|
}
|
|
1447
|
-
applyAclPayload(payload, signerRole) {
|
|
1741
|
+
applyAclPayload(payload, signerRole, options) {
|
|
1448
1742
|
if (!isAclPatch(payload.patch))
|
|
1449
1743
|
return false;
|
|
1450
1744
|
const patch = payload.patch;
|
|
1451
|
-
if (!
|
|
1452
|
-
|
|
1745
|
+
if (!options?.skipAuth) {
|
|
1746
|
+
if (!signerRole)
|
|
1747
|
+
return false;
|
|
1748
|
+
if (!this.canWriteAclTarget(signerRole, patch.role, patch.target, payload.stamp))
|
|
1749
|
+
return false;
|
|
1750
|
+
}
|
|
1453
1751
|
const assignment = {
|
|
1454
1752
|
id: patch.id,
|
|
1455
1753
|
actorId: patch.target,
|
|
1456
1754
|
role: patch.role,
|
|
1457
1755
|
stamp: payload.stamp,
|
|
1458
1756
|
by: payload.iss,
|
|
1757
|
+
publicKeyJwk: patch.publicKeyJwk,
|
|
1459
1758
|
};
|
|
1460
1759
|
const accepted = this.aclLog.merge(assignment);
|
|
1461
1760
|
if (accepted.length)
|
|
@@ -23,4 +23,7 @@ export declare function verifyToken(publicJwk: JsonWebKey, token: string, expect
|
|
|
23
23
|
header: SignedHeader;
|
|
24
24
|
payload: unknown;
|
|
25
25
|
} | false>;
|
|
26
|
+
export declare function validateActorKeyPair(privateJwk: JsonWebKey, publicJwk: JsonWebKey): Promise<void>;
|
|
27
|
+
export declare function signDetached(privateJwk: JsonWebKey, payload: string): Promise<string>;
|
|
28
|
+
export declare function verifyDetached(publicJwk: JsonWebKey, payload: string, signatureB64: string): Promise<boolean>;
|
|
26
29
|
export {};
|
package/dist/Dacument/crypto.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Bytes } from "bytecodec";
|
|
2
2
|
import { SigningAgent, VerificationAgent } from "zeyra";
|
|
3
|
+
const ACTOR_CHALLENGE = Bytes.fromString("dacument-actor-verify");
|
|
3
4
|
function stableStringify(value) {
|
|
4
5
|
if (value === null || typeof value !== "object")
|
|
5
6
|
return JSON.stringify(value);
|
|
@@ -16,6 +17,11 @@ function decodePart(part) {
|
|
|
16
17
|
const json = Bytes.toString(bytes);
|
|
17
18
|
return JSON.parse(json);
|
|
18
19
|
}
|
|
20
|
+
function toArrayBuffer(bytes) {
|
|
21
|
+
const buffer = new ArrayBuffer(bytes.byteLength);
|
|
22
|
+
new Uint8Array(buffer).set(bytes);
|
|
23
|
+
return buffer;
|
|
24
|
+
}
|
|
19
25
|
export async function signToken(privateJwk, header, payload) {
|
|
20
26
|
const headerJson = stableStringify(header);
|
|
21
27
|
const payloadJson = stableStringify(payload);
|
|
@@ -59,6 +65,24 @@ export async function verifyToken(publicJwk, token, expectedTyp) {
|
|
|
59
65
|
const verifier = new VerificationAgent(publicJwk);
|
|
60
66
|
const signingInput = Bytes.fromString(`${headerB64}.${payloadB64}`);
|
|
61
67
|
const signatureBytes = new Uint8Array(signature);
|
|
62
|
-
const ok = await verifier.verify(signingInput, signatureBytes
|
|
68
|
+
const ok = await verifier.verify(signingInput, toArrayBuffer(signatureBytes));
|
|
63
69
|
return ok ? { header, payload } : false;
|
|
64
70
|
}
|
|
71
|
+
export async function validateActorKeyPair(privateJwk, publicJwk) {
|
|
72
|
+
const signer = new SigningAgent(privateJwk);
|
|
73
|
+
const signatureBytes = new Uint8Array(await signer.sign(ACTOR_CHALLENGE));
|
|
74
|
+
const verifier = new VerificationAgent(publicJwk);
|
|
75
|
+
const ok = await verifier.verify(ACTOR_CHALLENGE, toArrayBuffer(signatureBytes));
|
|
76
|
+
if (!ok)
|
|
77
|
+
throw new Error("Dacument.setActorInfo: publicKeyJwk does not match privateKeyJwk");
|
|
78
|
+
}
|
|
79
|
+
export async function signDetached(privateJwk, payload) {
|
|
80
|
+
const signer = new SigningAgent(privateJwk);
|
|
81
|
+
const signature = await signer.sign(Bytes.fromString(payload));
|
|
82
|
+
return Bytes.toBase64UrlString(signature);
|
|
83
|
+
}
|
|
84
|
+
export async function verifyDetached(publicJwk, payload, signatureB64) {
|
|
85
|
+
const verifier = new VerificationAgent(publicJwk);
|
|
86
|
+
const signatureBytes = Bytes.fromBase64UrlString(signatureB64);
|
|
87
|
+
return verifier.verify(Bytes.fromString(payload), toArrayBuffer(signatureBytes));
|
|
88
|
+
}
|
package/dist/Dacument/types.d.ts
CHANGED
|
@@ -27,6 +27,11 @@ export type RolePublicKeys = {
|
|
|
27
27
|
manager: JsonWebKey;
|
|
28
28
|
editor: JsonWebKey;
|
|
29
29
|
};
|
|
30
|
+
export type ActorInfo = {
|
|
31
|
+
id: string;
|
|
32
|
+
privateKeyJwk: JsonWebKey;
|
|
33
|
+
publicKeyJwk: JsonWebKey;
|
|
34
|
+
};
|
|
30
35
|
export type RegisterSchema<T extends JsTypeName = JsTypeName> = {
|
|
31
36
|
crdt: "register";
|
|
32
37
|
jsType: T;
|
|
@@ -77,6 +82,7 @@ export type OpPayload = {
|
|
|
77
82
|
};
|
|
78
83
|
export type SignedOp = {
|
|
79
84
|
token: string;
|
|
85
|
+
actorSig?: string;
|
|
80
86
|
};
|
|
81
87
|
export type DacumentChangeEvent = {
|
|
82
88
|
type: "change";
|
|
@@ -112,6 +118,23 @@ export type AclAssignment = {
|
|
|
112
118
|
role: Role;
|
|
113
119
|
stamp: HLCStamp;
|
|
114
120
|
by: string;
|
|
121
|
+
publicKeyJwk?: JsonWebKey;
|
|
122
|
+
};
|
|
123
|
+
export type VerifyActorIntegrityOptions = {
|
|
124
|
+
token?: string | SignedOp;
|
|
125
|
+
ops?: Array<string | SignedOp>;
|
|
126
|
+
snapshot?: DocSnapshot;
|
|
127
|
+
};
|
|
128
|
+
export type VerificationFailure = {
|
|
129
|
+
index: number;
|
|
130
|
+
reason: string;
|
|
131
|
+
};
|
|
132
|
+
export type VerificationResult = {
|
|
133
|
+
ok: boolean;
|
|
134
|
+
verified: number;
|
|
135
|
+
failed: number;
|
|
136
|
+
missing: number;
|
|
137
|
+
failures: VerificationFailure[];
|
|
115
138
|
};
|
|
116
139
|
export type DocSnapshot = {
|
|
117
140
|
docId: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dacument",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Schema-driven CRDT document with signed ops
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Schema-driven CRDT document with signed ops, role-based ACLs, and optional per-actor verification.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"crdt",
|
|
7
7
|
"document",
|