cojson 0.8.34 → 0.8.35
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/CHANGELOG.md +9 -0
- package/dist/native/coValueCore.js +73 -37
- package/dist/native/coValueCore.js.map +1 -1
- package/dist/native/coValues/coMap.js +2 -2
- package/dist/native/coValues/coMap.js.map +1 -1
- package/dist/native/coValues/group.js +132 -5
- package/dist/native/coValues/group.js.map +1 -1
- package/dist/native/exports.js +5 -2
- package/dist/native/exports.js.map +1 -1
- package/dist/native/ids.js +33 -0
- package/dist/native/ids.js.map +1 -1
- package/dist/native/permissions.js +206 -145
- package/dist/native/permissions.js.map +1 -1
- package/dist/native/storage/index.js +8 -4
- package/dist/native/storage/index.js.map +1 -1
- package/dist/native/sync.js +41 -25
- package/dist/native/sync.js.map +1 -1
- package/dist/web/coValueCore.js +73 -37
- package/dist/web/coValueCore.js.map +1 -1
- package/dist/web/coValues/coMap.js +2 -2
- package/dist/web/coValues/coMap.js.map +1 -1
- package/dist/web/coValues/group.js +132 -5
- package/dist/web/coValues/group.js.map +1 -1
- package/dist/web/exports.js +5 -2
- package/dist/web/exports.js.map +1 -1
- package/dist/web/ids.js +33 -0
- package/dist/web/ids.js.map +1 -1
- package/dist/web/permissions.js +206 -145
- package/dist/web/permissions.js.map +1 -1
- package/dist/web/storage/index.js +8 -4
- package/dist/web/storage/index.js.map +1 -1
- package/dist/web/sync.js +41 -25
- package/dist/web/sync.js.map +1 -1
- package/package.json +1 -1
- package/src/coValueCore.ts +119 -46
- package/src/coValues/coMap.ts +3 -6
- package/src/coValues/group.ts +219 -6
- package/src/exports.ts +18 -3
- package/src/ids.ts +48 -0
- package/src/permissions.ts +297 -204
- package/src/storage/index.ts +12 -4
- package/src/sync.ts +43 -26
- package/src/tests/group.test.ts +152 -1
- package/src/tests/permissions.test.ts +785 -2
- package/src/tests/sync.test.ts +29 -0
- package/src/tests/testUtils.ts +102 -1
package/src/exports.ts
CHANGED
|
@@ -23,8 +23,14 @@ import {
|
|
|
23
23
|
secretSeedLength,
|
|
24
24
|
shortHashLength,
|
|
25
25
|
} from "./crypto/crypto.js";
|
|
26
|
-
import {
|
|
27
|
-
|
|
26
|
+
import {
|
|
27
|
+
getGroupDependentKey,
|
|
28
|
+
getGroupDependentKeyList,
|
|
29
|
+
isRawCoID,
|
|
30
|
+
rawCoIDfromBytes,
|
|
31
|
+
rawCoIDtoBytes,
|
|
32
|
+
} from "./ids.js";
|
|
33
|
+
import { Stringified, parseJSON } from "./jsonStringify.js";
|
|
28
34
|
import { LocalNode } from "./localNode.js";
|
|
29
35
|
import type { Role } from "./permissions.js";
|
|
30
36
|
import { Channel, connectedPeers } from "./streamUtils.js";
|
|
@@ -53,7 +59,11 @@ import type {
|
|
|
53
59
|
Peer,
|
|
54
60
|
SyncMessage,
|
|
55
61
|
} from "./sync.js";
|
|
56
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
DisconnectedError,
|
|
64
|
+
PingTimeoutError,
|
|
65
|
+
emptyKnownState,
|
|
66
|
+
} from "./sync.js";
|
|
57
67
|
|
|
58
68
|
type Value = JsonValue | AnyRawCoValue;
|
|
59
69
|
|
|
@@ -79,6 +89,8 @@ export const cojsonInternals = {
|
|
|
79
89
|
StreamingHash,
|
|
80
90
|
Channel,
|
|
81
91
|
getPriorityFromHeader,
|
|
92
|
+
getGroupDependentKeyList,
|
|
93
|
+
getGroupDependentKey,
|
|
82
94
|
};
|
|
83
95
|
|
|
84
96
|
export {
|
|
@@ -116,6 +128,7 @@ export {
|
|
|
116
128
|
SyncMessage,
|
|
117
129
|
isRawCoID,
|
|
118
130
|
LSMStorage,
|
|
131
|
+
emptyKnownState,
|
|
119
132
|
};
|
|
120
133
|
|
|
121
134
|
export type {
|
|
@@ -128,6 +141,7 @@ export type {
|
|
|
128
141
|
DisconnectedError,
|
|
129
142
|
PingTimeoutError,
|
|
130
143
|
CoValueUniqueness,
|
|
144
|
+
Stringified,
|
|
131
145
|
};
|
|
132
146
|
|
|
133
147
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
@@ -137,6 +151,7 @@ export namespace CojsonInternalTypes {
|
|
|
137
151
|
export type KnownStateMessage = import("./sync.js").KnownStateMessage;
|
|
138
152
|
export type LoadMessage = import("./sync.js").LoadMessage;
|
|
139
153
|
export type NewContentMessage = import("./sync.js").NewContentMessage;
|
|
154
|
+
export type SessionNewContent = import("./sync.js").SessionNewContent;
|
|
140
155
|
export type CoValueHeader = import("./coValueCore.js").CoValueHeader;
|
|
141
156
|
export type Transaction = import("./coValueCore.js").Transaction;
|
|
142
157
|
export type TransactionID = import("./ids.js").TransactionID;
|
package/src/ids.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { base58 } from "@scure/base";
|
|
2
|
+
import { CoID } from "./coValue.js";
|
|
2
3
|
import { RawAccountID } from "./coValues/account.js";
|
|
3
4
|
import { shortHashLength } from "./crypto/crypto.js";
|
|
5
|
+
import { RawGroup } from "./exports.js";
|
|
4
6
|
|
|
5
7
|
export type RawCoID = `co_z${string}`;
|
|
8
|
+
export type ParentGroupReference = `parent_${CoID<RawGroup>}`;
|
|
9
|
+
export type ChildGroupReference = `child_${CoID<RawGroup>}`;
|
|
6
10
|
|
|
7
11
|
export function isRawCoID(id: unknown): id is RawCoID {
|
|
8
12
|
return typeof id === "string" && id.startsWith("co_z");
|
|
@@ -29,3 +33,47 @@ export function isAgentID(id: string): id is AgentID {
|
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
export type SessionID = `${RawAccountID | AgentID}_session_z${string}`;
|
|
36
|
+
|
|
37
|
+
export function isParentGroupReference(
|
|
38
|
+
key: string,
|
|
39
|
+
): key is ParentGroupReference {
|
|
40
|
+
return key.startsWith("parent_");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getParentGroupId(key: ParentGroupReference): CoID<RawGroup> {
|
|
44
|
+
return key.slice("parent_".length) as CoID<RawGroup>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isChildGroupReference(key: string): key is ChildGroupReference {
|
|
48
|
+
return key.startsWith("child_");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getChildGroupId(key: ChildGroupReference): CoID<RawGroup> {
|
|
52
|
+
return key.slice("child_".length) as CoID<RawGroup>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getGroupDependentKey(key: unknown) {
|
|
56
|
+
if (typeof key !== "string") return undefined;
|
|
57
|
+
|
|
58
|
+
if (isParentGroupReference(key)) {
|
|
59
|
+
return getParentGroupId(key);
|
|
60
|
+
} else if (key.startsWith("co_")) {
|
|
61
|
+
return key as RawCoID;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getGroupDependentKeyList(keys: unknown[]) {
|
|
68
|
+
const groupDependentKeys: RawCoID[] = [];
|
|
69
|
+
|
|
70
|
+
for (const key of keys) {
|
|
71
|
+
const value = getGroupDependentKey(key);
|
|
72
|
+
|
|
73
|
+
if (value) {
|
|
74
|
+
groupDependentKeys.push(value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return groupDependentKeys;
|
|
79
|
+
}
|
package/src/permissions.ts
CHANGED
|
@@ -2,9 +2,16 @@ import { CoID } from "./coValue.js";
|
|
|
2
2
|
import { CoValueCore, Transaction } from "./coValueCore.js";
|
|
3
3
|
import { RawAccount, RawAccountID, RawProfile } from "./coValues/account.js";
|
|
4
4
|
import { MapOpPayload } from "./coValues/coMap.js";
|
|
5
|
-
import { EVERYONE, Everyone } from "./coValues/group.js";
|
|
5
|
+
import { EVERYONE, Everyone, RawGroup } from "./coValues/group.js";
|
|
6
6
|
import { KeyID } from "./crypto/crypto.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
AgentID,
|
|
9
|
+
ParentGroupReference,
|
|
10
|
+
RawCoID,
|
|
11
|
+
SessionID,
|
|
12
|
+
TransactionID,
|
|
13
|
+
getParentGroupId,
|
|
14
|
+
} from "./ids.js";
|
|
8
15
|
import { parseJSON } from "./jsonStringify.js";
|
|
9
16
|
import { JsonValue } from "./jsonValue.js";
|
|
10
17
|
import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
|
|
@@ -24,202 +31,20 @@ export type Role =
|
|
|
24
31
|
| "writerInvite"
|
|
25
32
|
| "readerInvite";
|
|
26
33
|
|
|
34
|
+
type ValidTransactionsResult = { txID: TransactionID; tx: Transaction };
|
|
35
|
+
type MemberState = { [agent: RawAccountID | AgentID]: Role; [EVERYONE]?: Role };
|
|
36
|
+
|
|
27
37
|
export function determineValidTransactions(
|
|
28
38
|
coValue: CoValueCore,
|
|
29
39
|
): { txID: TransactionID; tx: Transaction }[] {
|
|
30
40
|
if (coValue.header.ruleset.type === "group") {
|
|
31
|
-
const allTransactionsSorted = [...coValue.sessionLogs.entries()].flatMap(
|
|
32
|
-
([sessionID, sessionLog]) => {
|
|
33
|
-
return sessionLog.transactions.map((tx, txIndex) => ({
|
|
34
|
-
sessionID,
|
|
35
|
-
txIndex,
|
|
36
|
-
tx,
|
|
37
|
-
})) as {
|
|
38
|
-
sessionID: SessionID;
|
|
39
|
-
txIndex: number;
|
|
40
|
-
tx: Transaction;
|
|
41
|
-
}[];
|
|
42
|
-
},
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
allTransactionsSorted.sort((a, b) => {
|
|
46
|
-
return a.tx.madeAt - b.tx.madeAt;
|
|
47
|
-
});
|
|
48
|
-
|
|
49
41
|
const initialAdmin = coValue.header.ruleset.initialAdmin;
|
|
50
|
-
|
|
51
42
|
if (!initialAdmin) {
|
|
52
43
|
throw new Error("Group must have initialAdmin");
|
|
53
44
|
}
|
|
54
45
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
[EVERYONE]?: Role;
|
|
58
|
-
} = {};
|
|
59
|
-
|
|
60
|
-
const validTransactions: { txID: TransactionID; tx: Transaction }[] = [];
|
|
61
|
-
|
|
62
|
-
for (const { sessionID, txIndex, tx } of allTransactionsSorted) {
|
|
63
|
-
// console.log("before", { memberState, validTransactions });
|
|
64
|
-
const transactor = accountOrAgentIDfromSessionID(sessionID);
|
|
65
|
-
|
|
66
|
-
if (tx.privacy === "private") {
|
|
67
|
-
if (memberState[transactor] === "admin") {
|
|
68
|
-
validTransactions.push({
|
|
69
|
-
txID: { sessionID, txIndex },
|
|
70
|
-
tx,
|
|
71
|
-
});
|
|
72
|
-
continue;
|
|
73
|
-
} else {
|
|
74
|
-
console.warn("Only admins can make private transactions in groups");
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
let changes;
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
changes = parseJSON(tx.changes);
|
|
83
|
-
} catch (e) {
|
|
84
|
-
console.warn(
|
|
85
|
-
coValue.id,
|
|
86
|
-
"Invalid JSON in transaction",
|
|
87
|
-
e,
|
|
88
|
-
tx,
|
|
89
|
-
JSON.stringify(tx.changes, (k, v) =>
|
|
90
|
-
k === "changes" || k === "encryptedChanges"
|
|
91
|
-
? v.slice(0, 20) + "..."
|
|
92
|
-
: v,
|
|
93
|
-
),
|
|
94
|
-
);
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const change = changes[0] as
|
|
99
|
-
| MapOpPayload<RawAccountID | AgentID | Everyone, Role>
|
|
100
|
-
| MapOpPayload<"readKey", JsonValue>
|
|
101
|
-
| MapOpPayload<"profile", CoID<RawProfile>>;
|
|
102
|
-
if (changes.length !== 1) {
|
|
103
|
-
console.warn("Group transaction must have exactly one change");
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (change.op !== "set") {
|
|
108
|
-
console.warn("Group transaction must set a role or readKey");
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (change.key === "readKey") {
|
|
113
|
-
if (memberState[transactor] !== "admin") {
|
|
114
|
-
console.warn("Only admins can set readKeys");
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
119
|
-
continue;
|
|
120
|
-
} else if (change.key === "profile") {
|
|
121
|
-
if (memberState[transactor] !== "admin") {
|
|
122
|
-
console.warn("Only admins can set profile");
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
127
|
-
continue;
|
|
128
|
-
} else if (
|
|
129
|
-
isKeyForKeyField(change.key) ||
|
|
130
|
-
isKeyForAccountField(change.key)
|
|
131
|
-
) {
|
|
132
|
-
if (
|
|
133
|
-
memberState[transactor] !== "admin" &&
|
|
134
|
-
memberState[transactor] !== "adminInvite" &&
|
|
135
|
-
memberState[transactor] !== "writerInvite" &&
|
|
136
|
-
memberState[transactor] !== "readerInvite"
|
|
137
|
-
) {
|
|
138
|
-
console.warn("Only admins can reveal keys");
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// TODO: check validity of agents who the key is revealed to?
|
|
143
|
-
|
|
144
|
-
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const affectedMember = change.key;
|
|
149
|
-
const assignedRole = change.value;
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
change.value !== "admin" &&
|
|
153
|
-
change.value !== "writer" &&
|
|
154
|
-
change.value !== "reader" &&
|
|
155
|
-
change.value !== "revoked" &&
|
|
156
|
-
change.value !== "adminInvite" &&
|
|
157
|
-
change.value !== "writerInvite" &&
|
|
158
|
-
change.value !== "readerInvite"
|
|
159
|
-
) {
|
|
160
|
-
console.warn("Group transaction must set a valid role");
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
affectedMember === EVERYONE &&
|
|
166
|
-
!(
|
|
167
|
-
change.value === "reader" ||
|
|
168
|
-
change.value === "writer" ||
|
|
169
|
-
change.value === "revoked"
|
|
170
|
-
)
|
|
171
|
-
) {
|
|
172
|
-
console.warn("Everyone can only be set to reader, writer or revoked");
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const isFirstSelfAppointment =
|
|
177
|
-
!memberState[transactor] &&
|
|
178
|
-
transactor === initialAdmin &&
|
|
179
|
-
change.op === "set" &&
|
|
180
|
-
change.key === transactor &&
|
|
181
|
-
change.value === "admin";
|
|
182
|
-
|
|
183
|
-
if (!isFirstSelfAppointment) {
|
|
184
|
-
if (memberState[transactor] === "admin") {
|
|
185
|
-
if (
|
|
186
|
-
memberState[affectedMember] === "admin" &&
|
|
187
|
-
affectedMember !== transactor &&
|
|
188
|
-
assignedRole !== "admin"
|
|
189
|
-
) {
|
|
190
|
-
console.warn("Admins can only demote themselves.");
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
} else if (memberState[transactor] === "adminInvite") {
|
|
194
|
-
if (change.value !== "admin") {
|
|
195
|
-
console.warn("AdminInvites can only create admins.");
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
} else if (memberState[transactor] === "writerInvite") {
|
|
199
|
-
if (change.value !== "writer") {
|
|
200
|
-
console.warn("WriterInvites can only create writers.");
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
} else if (memberState[transactor] === "readerInvite") {
|
|
204
|
-
if (change.value !== "reader") {
|
|
205
|
-
console.warn("ReaderInvites can only create reader.");
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
} else {
|
|
209
|
-
console.warn(
|
|
210
|
-
"Group transaction must be made by current admin or invite",
|
|
211
|
-
);
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
memberState[affectedMember] = change.value;
|
|
217
|
-
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
218
|
-
|
|
219
|
-
// console.log("after", { memberState, validTransactions });
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return validTransactions;
|
|
46
|
+
return determineValidTransactionsForGroup(coValue, initialAdmin)
|
|
47
|
+
.validTransactions;
|
|
223
48
|
} else if (coValue.header.ruleset.type === "ownedByGroup") {
|
|
224
49
|
const groupContent = expectGroup(
|
|
225
50
|
coValue.node
|
|
@@ -241,27 +66,18 @@ export function determineValidTransactions(
|
|
|
241
66
|
return sessionLog.transactions
|
|
242
67
|
.filter((tx) => {
|
|
243
68
|
const groupAtTime = groupContent.atTime(tx.madeAt);
|
|
244
|
-
const effectiveTransactor =
|
|
245
|
-
transactor
|
|
246
|
-
groupAtTime
|
|
247
|
-
|
|
248
|
-
(agentID) => agentID,
|
|
249
|
-
(e) => {
|
|
250
|
-
console.error(
|
|
251
|
-
"Error while determining current agent ID in valid transactions",
|
|
252
|
-
e,
|
|
253
|
-
);
|
|
254
|
-
return undefined;
|
|
255
|
-
},
|
|
256
|
-
)
|
|
257
|
-
: transactor;
|
|
69
|
+
const effectiveTransactor = agentInAccountOrMemberInGroup(
|
|
70
|
+
transactor,
|
|
71
|
+
groupAtTime,
|
|
72
|
+
);
|
|
258
73
|
|
|
259
74
|
if (!effectiveTransactor) {
|
|
260
75
|
return false;
|
|
261
76
|
}
|
|
262
77
|
|
|
263
78
|
const transactorRoleAtTxTime =
|
|
264
|
-
groupAtTime.
|
|
79
|
+
groupAtTime.roleOfInternal(effectiveTransactor)?.role ||
|
|
80
|
+
groupAtTime.roleOfInternal(EVERYONE)?.role;
|
|
265
81
|
|
|
266
82
|
return (
|
|
267
83
|
transactorRoleAtTxTime === "admin" ||
|
|
@@ -291,6 +107,275 @@ export function determineValidTransactions(
|
|
|
291
107
|
}
|
|
292
108
|
}
|
|
293
109
|
|
|
110
|
+
function isHigherRole(a: Role, b: Role | undefined) {
|
|
111
|
+
if (a === undefined) return false;
|
|
112
|
+
if (b === undefined) return true;
|
|
113
|
+
if (b === "admin") return false;
|
|
114
|
+
if (a === "admin") return true;
|
|
115
|
+
|
|
116
|
+
return a === "writer" && b === "reader";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveMemberStateFromParentReference(
|
|
120
|
+
coValue: CoValueCore,
|
|
121
|
+
memberState: MemberState,
|
|
122
|
+
parentReference: ParentGroupReference,
|
|
123
|
+
) {
|
|
124
|
+
const parentGroup = coValue.node.expectCoValueLoaded(
|
|
125
|
+
getParentGroupId(parentReference),
|
|
126
|
+
"Expected parent group to be loaded",
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (parentGroup.header.ruleset.type !== "group") {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const initialAdmin = parentGroup.header.ruleset.initialAdmin;
|
|
134
|
+
|
|
135
|
+
if (!initialAdmin) {
|
|
136
|
+
throw new Error("Group must have initialAdmin");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const { memberState: parentGroupMemberState } =
|
|
140
|
+
determineValidTransactionsForGroup(parentGroup, initialAdmin);
|
|
141
|
+
|
|
142
|
+
for (const agent of Object.keys(parentGroupMemberState) as Array<
|
|
143
|
+
keyof MemberState
|
|
144
|
+
>) {
|
|
145
|
+
const parentRole = parentGroupMemberState[agent];
|
|
146
|
+
const currentRole = memberState[agent];
|
|
147
|
+
|
|
148
|
+
if (parentRole && isHigherRole(parentRole, currentRole)) {
|
|
149
|
+
memberState[agent] = parentRole;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function determineValidTransactionsForGroup(
|
|
155
|
+
coValue: CoValueCore,
|
|
156
|
+
initialAdmin: RawAccountID | AgentID,
|
|
157
|
+
): { validTransactions: ValidTransactionsResult[]; memberState: MemberState } {
|
|
158
|
+
const allTransactionsSorted = [...coValue.sessionLogs.entries()].flatMap(
|
|
159
|
+
([sessionID, sessionLog]) => {
|
|
160
|
+
return sessionLog.transactions.map((tx, txIndex) => ({
|
|
161
|
+
sessionID,
|
|
162
|
+
txIndex,
|
|
163
|
+
tx,
|
|
164
|
+
})) as {
|
|
165
|
+
sessionID: SessionID;
|
|
166
|
+
txIndex: number;
|
|
167
|
+
tx: Transaction;
|
|
168
|
+
}[];
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
allTransactionsSorted.sort((a, b) => {
|
|
173
|
+
return a.tx.madeAt - b.tx.madeAt;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const memberState: MemberState = {};
|
|
177
|
+
const validTransactions: ValidTransactionsResult[] = [];
|
|
178
|
+
|
|
179
|
+
for (const { sessionID, txIndex, tx } of allTransactionsSorted) {
|
|
180
|
+
// console.log("before", { memberState, validTransactions });
|
|
181
|
+
const transactor = accountOrAgentIDfromSessionID(sessionID);
|
|
182
|
+
|
|
183
|
+
if (tx.privacy === "private") {
|
|
184
|
+
if (memberState[transactor] === "admin") {
|
|
185
|
+
validTransactions.push({
|
|
186
|
+
txID: { sessionID, txIndex },
|
|
187
|
+
tx,
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
} else {
|
|
191
|
+
console.warn("Only admins can make private transactions in groups");
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let changes;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
changes = parseJSON(tx.changes);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
console.warn(
|
|
202
|
+
coValue.id,
|
|
203
|
+
"Invalid JSON in transaction",
|
|
204
|
+
e,
|
|
205
|
+
tx,
|
|
206
|
+
JSON.stringify(tx.changes, (k, v) =>
|
|
207
|
+
k === "changes" || k === "encryptedChanges"
|
|
208
|
+
? v.slice(0, 20) + "..."
|
|
209
|
+
: v,
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const change = changes[0] as
|
|
216
|
+
| MapOpPayload<RawAccountID | AgentID | Everyone, Role>
|
|
217
|
+
| MapOpPayload<"readKey", JsonValue>
|
|
218
|
+
| MapOpPayload<"profile", CoID<RawProfile>>
|
|
219
|
+
| MapOpPayload<`parent_${CoID<RawGroup>}`, CoID<RawGroup>>
|
|
220
|
+
| MapOpPayload<`child_${CoID<RawGroup>}`, CoID<RawGroup>>;
|
|
221
|
+
|
|
222
|
+
if (changes.length !== 1) {
|
|
223
|
+
console.warn("Group transaction must have exactly one change");
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (change.op !== "set") {
|
|
228
|
+
console.warn("Group transaction must set a role or readKey");
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (change.key === "readKey") {
|
|
233
|
+
if (memberState[transactor] !== "admin") {
|
|
234
|
+
console.warn("Only admins can set readKeys");
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
239
|
+
continue;
|
|
240
|
+
} else if (change.key === "profile") {
|
|
241
|
+
if (memberState[transactor] !== "admin") {
|
|
242
|
+
console.warn("Only admins can set profile");
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
247
|
+
continue;
|
|
248
|
+
} else if (
|
|
249
|
+
isKeyForKeyField(change.key) ||
|
|
250
|
+
isKeyForAccountField(change.key)
|
|
251
|
+
) {
|
|
252
|
+
if (
|
|
253
|
+
memberState[transactor] !== "admin" &&
|
|
254
|
+
memberState[transactor] !== "adminInvite" &&
|
|
255
|
+
memberState[transactor] !== "writerInvite" &&
|
|
256
|
+
memberState[transactor] !== "readerInvite"
|
|
257
|
+
) {
|
|
258
|
+
console.warn("Only admins can reveal keys");
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// TODO: check validity of agents who the key is revealed to?
|
|
263
|
+
|
|
264
|
+
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
265
|
+
continue;
|
|
266
|
+
} else if (isParentExtension(change.key)) {
|
|
267
|
+
if (memberState[transactor] !== "admin") {
|
|
268
|
+
console.warn("Only admins can set parent extensions");
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
resolveMemberStateFromParentReference(coValue, memberState, change.key);
|
|
272
|
+
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
273
|
+
continue;
|
|
274
|
+
} else if (isChildExtension(change.key)) {
|
|
275
|
+
if (memberState[transactor] !== "admin") {
|
|
276
|
+
console.warn("Only admins can set child extensions");
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const affectedMember = change.key;
|
|
284
|
+
const assignedRole = change.value;
|
|
285
|
+
|
|
286
|
+
if (
|
|
287
|
+
change.value !== "admin" &&
|
|
288
|
+
change.value !== "writer" &&
|
|
289
|
+
change.value !== "reader" &&
|
|
290
|
+
change.value !== "revoked" &&
|
|
291
|
+
change.value !== "adminInvite" &&
|
|
292
|
+
change.value !== "writerInvite" &&
|
|
293
|
+
change.value !== "readerInvite"
|
|
294
|
+
) {
|
|
295
|
+
console.warn("Group transaction must set a valid role");
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (
|
|
300
|
+
affectedMember === EVERYONE &&
|
|
301
|
+
!(
|
|
302
|
+
change.value === "reader" ||
|
|
303
|
+
change.value === "writer" ||
|
|
304
|
+
change.value === "revoked"
|
|
305
|
+
)
|
|
306
|
+
) {
|
|
307
|
+
console.warn("Everyone can only be set to reader, writer or revoked");
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const isFirstSelfAppointment =
|
|
312
|
+
!memberState[transactor] &&
|
|
313
|
+
transactor === initialAdmin &&
|
|
314
|
+
change.op === "set" &&
|
|
315
|
+
change.key === transactor &&
|
|
316
|
+
change.value === "admin";
|
|
317
|
+
|
|
318
|
+
if (!isFirstSelfAppointment) {
|
|
319
|
+
if (memberState[transactor] === "admin") {
|
|
320
|
+
if (
|
|
321
|
+
memberState[affectedMember] === "admin" &&
|
|
322
|
+
affectedMember !== transactor &&
|
|
323
|
+
assignedRole !== "admin"
|
|
324
|
+
) {
|
|
325
|
+
console.warn("Admins can only demote themselves.");
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
} else if (memberState[transactor] === "adminInvite") {
|
|
329
|
+
if (change.value !== "admin") {
|
|
330
|
+
console.warn("AdminInvites can only create admins.");
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
} else if (memberState[transactor] === "writerInvite") {
|
|
334
|
+
if (change.value !== "writer") {
|
|
335
|
+
console.warn("WriterInvites can only create writers.");
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
} else if (memberState[transactor] === "readerInvite") {
|
|
339
|
+
if (change.value !== "reader") {
|
|
340
|
+
console.warn("ReaderInvites can only create reader.");
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
console.warn(
|
|
345
|
+
"Group transaction must be made by current admin or invite",
|
|
346
|
+
);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
memberState[affectedMember] = change.value;
|
|
352
|
+
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
353
|
+
|
|
354
|
+
// console.log("after", { memberState, validTransactions });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return { validTransactions, memberState };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function agentInAccountOrMemberInGroup(
|
|
361
|
+
transactor: RawAccountID | AgentID,
|
|
362
|
+
groupAtTime: RawGroup,
|
|
363
|
+
): RawAccountID | AgentID | undefined {
|
|
364
|
+
if (transactor === groupAtTime.id && groupAtTime instanceof RawAccount) {
|
|
365
|
+
return groupAtTime.currentAgentID().match(
|
|
366
|
+
(agentID) => agentID,
|
|
367
|
+
(e) => {
|
|
368
|
+
console.error(
|
|
369
|
+
"Error while determining current agent ID in valid transactions",
|
|
370
|
+
e,
|
|
371
|
+
);
|
|
372
|
+
return undefined;
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
return transactor;
|
|
377
|
+
}
|
|
378
|
+
|
|
294
379
|
export function isKeyForKeyField(co: string): co is `${KeyID}_for_${KeyID}` {
|
|
295
380
|
return co.startsWith("key_") && co.includes("_for_key");
|
|
296
381
|
}
|
|
@@ -304,3 +389,11 @@ export function isKeyForAccountField(
|
|
|
304
389
|
co.includes("_for_everyone")
|
|
305
390
|
);
|
|
306
391
|
}
|
|
392
|
+
|
|
393
|
+
function isParentExtension(key: string): key is `parent_${CoID<RawGroup>}` {
|
|
394
|
+
return key.startsWith("parent_");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function isChildExtension(key: string): key is `child_${CoID<RawGroup>}` {
|
|
398
|
+
return key.startsWith("child_");
|
|
399
|
+
}
|
package/src/storage/index.ts
CHANGED
|
@@ -146,7 +146,7 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
|
|
|
146
146
|
asDependencyOf || id,
|
|
147
147
|
);
|
|
148
148
|
} else if (!known?.header && coValue.header?.ruleset.type === "group") {
|
|
149
|
-
const
|
|
149
|
+
const dependedOnAccountsAndGroups = new Set();
|
|
150
150
|
for (const session of Object.values(coValue.sessionEntries)) {
|
|
151
151
|
for (const entry of session) {
|
|
152
152
|
for (const tx of entry.transactions) {
|
|
@@ -154,16 +154,24 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
|
|
|
154
154
|
const parsedChanges = JSON.parse(tx.changes);
|
|
155
155
|
for (const change of parsedChanges) {
|
|
156
156
|
if (change.op === "set" && change.key.startsWith("co_")) {
|
|
157
|
-
|
|
157
|
+
dependedOnAccountsAndGroups.add(change.key);
|
|
158
|
+
}
|
|
159
|
+
if (
|
|
160
|
+
change.op === "set" &&
|
|
161
|
+
change.key.startsWith("parent_co_")
|
|
162
|
+
) {
|
|
163
|
+
dependedOnAccountsAndGroups.add(
|
|
164
|
+
change.key.replace("parent_", ""),
|
|
165
|
+
);
|
|
158
166
|
}
|
|
159
167
|
}
|
|
160
168
|
}
|
|
161
169
|
}
|
|
162
170
|
}
|
|
163
171
|
}
|
|
164
|
-
for (const
|
|
172
|
+
for (const accountOrGroup of dependedOnAccountsAndGroups) {
|
|
165
173
|
await this.sendNewContent(
|
|
166
|
-
|
|
174
|
+
accountOrGroup as CoID<RawCoValue>,
|
|
167
175
|
undefined,
|
|
168
176
|
asDependencyOf || id,
|
|
169
177
|
);
|