cojson 0.0.21 → 0.0.23
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/dist/account.d.ts +1 -1
- package/dist/account.js +1 -1
- package/dist/account.js.map +1 -1
- package/dist/coValue.d.ts +10 -4
- package/dist/coValue.js +32 -15
- package/dist/coValue.js.map +1 -1
- package/dist/contentTypes/coMap.d.ts +2 -2
- package/dist/contentTypes/coMap.js +6 -6
- package/dist/contentTypes/coMap.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +2 -1
- package/dist/node.js +68 -11
- package/dist/node.js.map +1 -1
- package/dist/permissions.d.ts +4 -32
- package/dist/permissions.js +42 -106
- package/dist/permissions.js.map +1 -1
- package/dist/team.d.ts +37 -0
- package/dist/team.js +116 -0
- package/dist/team.js.map +1 -0
- package/dist/testUtils.d.ts +2 -2
- package/dist/testUtils.js +1 -1
- package/dist/testUtils.js.map +1 -1
- package/package.json +2 -2
- package/src/account.ts +1 -1
- package/src/coValue.test.ts +58 -0
- package/src/coValue.ts +41 -24
- package/src/contentTypes/coMap.ts +8 -8
- package/src/crypto.test.ts +10 -9
- package/src/index.ts +5 -0
- package/src/node.ts +151 -26
- package/src/permissions.test.ts +503 -3
- package/src/permissions.ts +70 -206
- package/src/sync.test.ts +8 -8
- package/src/team.ts +233 -0
- package/src/testUtils.ts +1 -1
- package/tsconfig.json +1 -0
- package/dist/account.test.d.ts +0 -1
- package/dist/account.test.js +0 -40
- package/dist/account.test.js.map +0 -1
- package/dist/coValue.test.d.ts +0 -1
- package/dist/coValue.test.js +0 -78
- package/dist/coValue.test.js.map +0 -1
- package/dist/contentType.test.d.ts +0 -1
- package/dist/contentType.test.js +0 -145
- package/dist/contentType.test.js.map +0 -1
- package/dist/crypto.test.d.ts +0 -1
- package/dist/crypto.test.js +0 -111
- package/dist/crypto.test.js.map +0 -1
- package/dist/permissions.test.d.ts +0 -1
- package/dist/permissions.test.js +0 -711
- package/dist/permissions.test.js.map +0 -1
- package/dist/sync.test.d.ts +0 -1
- package/dist/sync.test.js +0 -827
- package/dist/sync.test.js.map +0 -1
package/src/permissions.ts
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
import { CoID
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { CoID } from "./contentType.js";
|
|
2
|
+
import { MapOpPayload } from "./contentTypes/coMap.js";
|
|
3
|
+
import { JsonValue } from "./jsonValue.js";
|
|
4
4
|
import {
|
|
5
|
-
Encrypted,
|
|
6
5
|
KeyID,
|
|
7
|
-
KeySecret,
|
|
8
|
-
createdNowUnique,
|
|
9
|
-
newRandomKeySecret,
|
|
10
|
-
seal,
|
|
11
|
-
encryptKeySecret,
|
|
12
|
-
getAgentSealerID,
|
|
13
|
-
Sealed,
|
|
14
6
|
} from "./crypto.js";
|
|
15
7
|
import {
|
|
16
8
|
CoValue,
|
|
@@ -18,16 +10,25 @@ import {
|
|
|
18
10
|
TrustingTransaction,
|
|
19
11
|
accountOrAgentIDfromSessionID,
|
|
20
12
|
} from "./coValue.js";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
13
|
+
import { RawCoID, SessionID, TransactionID } from "./ids.js";
|
|
14
|
+
import {
|
|
15
|
+
AccountIDOrAgentID,
|
|
16
|
+
Profile,
|
|
17
|
+
} from "./account.js";
|
|
24
18
|
|
|
25
19
|
export type PermissionsDef =
|
|
26
20
|
| { type: "team"; initialAdmin: AccountIDOrAgentID }
|
|
27
21
|
| { type: "ownedByTeam"; team: RawCoID }
|
|
28
22
|
| { type: "unsafeAllowAll" };
|
|
29
23
|
|
|
30
|
-
export type Role =
|
|
24
|
+
export type Role =
|
|
25
|
+
| "reader"
|
|
26
|
+
| "writer"
|
|
27
|
+
| "admin"
|
|
28
|
+
| "revoked"
|
|
29
|
+
| "adminInvite"
|
|
30
|
+
| "writerInvite"
|
|
31
|
+
| "readerInvite";
|
|
31
32
|
|
|
32
33
|
export function determineValidTransactions(
|
|
33
34
|
coValue: CoValue
|
|
@@ -84,7 +85,7 @@ export function determineValidTransactions(
|
|
|
84
85
|
continue;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
if (change.op !== "
|
|
88
|
+
if (change.op !== "set") {
|
|
88
89
|
console.warn("Team transaction must set a role or readKey");
|
|
89
90
|
continue;
|
|
90
91
|
}
|
|
@@ -97,7 +98,7 @@ export function determineValidTransactions(
|
|
|
97
98
|
|
|
98
99
|
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
99
100
|
continue;
|
|
100
|
-
} else if (change.key ===
|
|
101
|
+
} else if (change.key === "profile") {
|
|
101
102
|
if (memberState[transactor] !== "admin") {
|
|
102
103
|
console.warn("Only admins can set profile");
|
|
103
104
|
continue;
|
|
@@ -105,8 +106,16 @@ export function determineValidTransactions(
|
|
|
105
106
|
|
|
106
107
|
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
107
108
|
continue;
|
|
108
|
-
} else if (
|
|
109
|
-
|
|
109
|
+
} else if (
|
|
110
|
+
isKeyForKeyField(change.key) ||
|
|
111
|
+
isKeyForAccountField(change.key)
|
|
112
|
+
) {
|
|
113
|
+
if (
|
|
114
|
+
memberState[transactor] !== "admin" &&
|
|
115
|
+
memberState[transactor] !== "adminInvite" &&
|
|
116
|
+
memberState[transactor] !== "writerInvite" &&
|
|
117
|
+
memberState[transactor] !== "readerInvite"
|
|
118
|
+
) {
|
|
110
119
|
console.warn("Only admins can reveal keys");
|
|
111
120
|
continue;
|
|
112
121
|
}
|
|
@@ -124,7 +133,10 @@ export function determineValidTransactions(
|
|
|
124
133
|
change.value !== "admin" &&
|
|
125
134
|
change.value !== "writer" &&
|
|
126
135
|
change.value !== "reader" &&
|
|
127
|
-
change.value !== "revoked"
|
|
136
|
+
change.value !== "revoked" &&
|
|
137
|
+
change.value !== "adminInvite" &&
|
|
138
|
+
change.value !== "writerInvite" &&
|
|
139
|
+
change.value !== "readerInvite"
|
|
128
140
|
) {
|
|
129
141
|
console.warn("Team transaction must set a valid role");
|
|
130
142
|
continue;
|
|
@@ -133,26 +145,41 @@ export function determineValidTransactions(
|
|
|
133
145
|
const isFirstSelfAppointment =
|
|
134
146
|
!memberState[transactor] &&
|
|
135
147
|
transactor === initialAdmin &&
|
|
136
|
-
change.op === "
|
|
148
|
+
change.op === "set" &&
|
|
137
149
|
change.key === transactor &&
|
|
138
150
|
change.value === "admin";
|
|
139
151
|
|
|
140
152
|
if (!isFirstSelfAppointment) {
|
|
141
|
-
if (memberState[transactor]
|
|
153
|
+
if (memberState[transactor] === "admin") {
|
|
154
|
+
if (
|
|
155
|
+
memberState[affectedMember] === "admin" &&
|
|
156
|
+
affectedMember !== transactor &&
|
|
157
|
+
assignedRole !== "admin"
|
|
158
|
+
) {
|
|
159
|
+
console.warn("Admins can only demote themselves.");
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
} else if (memberState[transactor] === "adminInvite") {
|
|
163
|
+
if (change.value !== "admin") {
|
|
164
|
+
console.warn("AdminInvites can only create admins.");
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
} else if (memberState[transactor] === "writerInvite") {
|
|
168
|
+
if (change.value !== "writer") {
|
|
169
|
+
console.warn("WriterInvites can only create writers.");
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
} else if (memberState[transactor] === "readerInvite") {
|
|
173
|
+
if (change.value !== "reader") {
|
|
174
|
+
console.warn("ReaderInvites can only create reader.");
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
142
178
|
console.warn(
|
|
143
|
-
"Team transaction must be made by current admin"
|
|
179
|
+
"Team transaction must be made by current admin or invite"
|
|
144
180
|
);
|
|
145
181
|
continue;
|
|
146
182
|
}
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
memberState[affectedMember] === "admin" &&
|
|
150
|
-
affectedMember !== transactor &&
|
|
151
|
-
assignedRole !== "admin"
|
|
152
|
-
) {
|
|
153
|
-
console.warn("Admins can only demote themselves.");
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
183
|
}
|
|
157
184
|
|
|
158
185
|
memberState[affectedMember] = change.value;
|
|
@@ -213,180 +240,17 @@ export function determineValidTransactions(
|
|
|
213
240
|
}
|
|
214
241
|
}
|
|
215
242
|
|
|
216
|
-
export
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
readKey: KeyID;
|
|
220
|
-
[revelationFor: `${KeyID}_for_${AccountIDOrAgentID}`]: Sealed<KeySecret>;
|
|
221
|
-
[oldKeyForNewKey: `${KeyID}_for_${KeyID}`]: Encrypted<
|
|
222
|
-
KeySecret,
|
|
223
|
-
{ encryptedID: KeyID; encryptingID: KeyID }
|
|
224
|
-
>;
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
export function expectTeamContent(
|
|
228
|
-
content: ContentType
|
|
229
|
-
): CoMap<TeamContent, JsonObject | null> {
|
|
230
|
-
if (content.type !== "comap") {
|
|
231
|
-
throw new Error("Expected map");
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return content as CoMap<TeamContent, JsonObject | null>;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export class Team {
|
|
238
|
-
teamMap: CoMap<TeamContent, JsonObject | null>;
|
|
239
|
-
node: LocalNode;
|
|
240
|
-
|
|
241
|
-
constructor(teamMap: CoMap<TeamContent, JsonObject | null>, node: LocalNode) {
|
|
242
|
-
this.teamMap = teamMap;
|
|
243
|
-
this.node = node;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
get id(): CoID<CoMap<TeamContent, JsonObject | null>> {
|
|
247
|
-
return this.teamMap.id;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
addMember(accountID: AccountIDOrAgentID, role: Role) {
|
|
251
|
-
this.teamMap = this.teamMap.edit((map) => {
|
|
252
|
-
const currentReadKey = this.teamMap.coValue.getCurrentReadKey();
|
|
253
|
-
|
|
254
|
-
if (!currentReadKey.secret) {
|
|
255
|
-
throw new Error("Can't add member without read key secret");
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const agent = this.node.resolveAccountAgent(
|
|
259
|
-
accountID,
|
|
260
|
-
"Expected to know agent to add them to team"
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
map.set(accountID, role, "trusting");
|
|
264
|
-
|
|
265
|
-
if (map.get(accountID) !== role) {
|
|
266
|
-
throw new Error("Failed to set role");
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
map.set(
|
|
270
|
-
`${currentReadKey.id}_for_${accountID}`,
|
|
271
|
-
seal(
|
|
272
|
-
currentReadKey.secret,
|
|
273
|
-
this.teamMap.coValue.node.account.currentSealerSecret(),
|
|
274
|
-
getAgentSealerID(agent),
|
|
275
|
-
{
|
|
276
|
-
in: this.teamMap.coValue.id,
|
|
277
|
-
tx: this.teamMap.coValue.nextTransactionID(),
|
|
278
|
-
}
|
|
279
|
-
),
|
|
280
|
-
"trusting"
|
|
281
|
-
);
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
rotateReadKey() {
|
|
286
|
-
const currentlyPermittedReaders = this.teamMap.keys().filter((key) => {
|
|
287
|
-
if (key.startsWith("co_") || isAgentID(key)) {
|
|
288
|
-
const role = this.teamMap.get(key);
|
|
289
|
-
return (
|
|
290
|
-
role === "admin" || role === "writer" || role === "reader"
|
|
291
|
-
);
|
|
292
|
-
} else {
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
}) as AccountIDOrAgentID[];
|
|
296
|
-
|
|
297
|
-
const maybeCurrentReadKey = this.teamMap.coValue.getCurrentReadKey();
|
|
298
|
-
|
|
299
|
-
if (!maybeCurrentReadKey.secret) {
|
|
300
|
-
throw new Error(
|
|
301
|
-
"Can't rotate read key secret we don't have access to"
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const currentReadKey = {
|
|
306
|
-
id: maybeCurrentReadKey.id,
|
|
307
|
-
secret: maybeCurrentReadKey.secret,
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
const newReadKey = newRandomKeySecret();
|
|
311
|
-
|
|
312
|
-
this.teamMap = this.teamMap.edit((map) => {
|
|
313
|
-
for (const readerID of currentlyPermittedReaders) {
|
|
314
|
-
const reader = this.node.resolveAccountAgent(
|
|
315
|
-
readerID,
|
|
316
|
-
"Expected to know currently permitted reader"
|
|
317
|
-
);
|
|
318
|
-
|
|
319
|
-
map.set(
|
|
320
|
-
`${newReadKey.id}_for_${readerID}`,
|
|
321
|
-
seal(
|
|
322
|
-
newReadKey.secret,
|
|
323
|
-
this.teamMap.coValue.node.account.currentSealerSecret(),
|
|
324
|
-
getAgentSealerID(reader),
|
|
325
|
-
{
|
|
326
|
-
in: this.teamMap.coValue.id,
|
|
327
|
-
tx: this.teamMap.coValue.nextTransactionID(),
|
|
328
|
-
}
|
|
329
|
-
),
|
|
330
|
-
"trusting"
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
map.set(
|
|
335
|
-
`${currentReadKey.id}_for_${newReadKey.id}`,
|
|
336
|
-
encryptKeySecret({
|
|
337
|
-
encrypting: newReadKey,
|
|
338
|
-
toEncrypt: currentReadKey,
|
|
339
|
-
}).encrypted,
|
|
340
|
-
"trusting"
|
|
341
|
-
);
|
|
342
|
-
|
|
343
|
-
map.set("readKey", newReadKey.id, "trusting");
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
removeMember(accountID: AccountIDOrAgentID) {
|
|
348
|
-
this.teamMap = this.teamMap.edit((map) => {
|
|
349
|
-
map.set(accountID, "revoked", "trusting");
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
this.rotateReadKey();
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
createMap<M extends { [key: string]: JsonValue }, Meta extends JsonObject | null = null>(
|
|
356
|
-
meta?: Meta
|
|
357
|
-
): CoMap<M, Meta> {
|
|
358
|
-
return this.node
|
|
359
|
-
.createCoValue({
|
|
360
|
-
type: "comap",
|
|
361
|
-
ruleset: {
|
|
362
|
-
type: "ownedByTeam",
|
|
363
|
-
team: this.teamMap.id,
|
|
364
|
-
},
|
|
365
|
-
meta: meta || null,
|
|
366
|
-
...createdNowUnique(),
|
|
367
|
-
})
|
|
368
|
-
.getCurrentContent() as CoMap<M, Meta>;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
testWithDifferentAccount(
|
|
372
|
-
account: GeneralizedControlledAccount,
|
|
373
|
-
sessionId: SessionID
|
|
374
|
-
): Team {
|
|
375
|
-
return new Team(
|
|
376
|
-
expectTeamContent(
|
|
377
|
-
this.teamMap.coValue
|
|
378
|
-
.testWithDifferentAccount(account, sessionId)
|
|
379
|
-
.getCurrentContent()
|
|
380
|
-
),
|
|
381
|
-
this.node
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
export function isKeyForKeyField(field: string): field is `${KeyID}_for_${KeyID}` {
|
|
243
|
+
export function isKeyForKeyField(
|
|
244
|
+
field: string
|
|
245
|
+
): field is `${KeyID}_for_${KeyID}` {
|
|
387
246
|
return field.startsWith("key_") && field.includes("_for_key");
|
|
388
247
|
}
|
|
389
248
|
|
|
390
|
-
export function isKeyForAccountField(
|
|
391
|
-
|
|
392
|
-
}
|
|
249
|
+
export function isKeyForAccountField(
|
|
250
|
+
field: string
|
|
251
|
+
): field is `${KeyID}_for_${AccountIDOrAgentID}` {
|
|
252
|
+
return (
|
|
253
|
+
field.startsWith("key_") &&
|
|
254
|
+
(field.includes("_for_sealer") || field.includes("_for_co"))
|
|
255
|
+
);
|
|
256
|
+
}
|
package/src/sync.test.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { LocalNode } from "./node.js";
|
|
|
3
3
|
import { Peer, PeerID, SyncMessage } from "./sync.js";
|
|
4
4
|
import { expectMap } from "./contentType.js";
|
|
5
5
|
import { MapOpPayload } from "./contentTypes/coMap.js";
|
|
6
|
-
import { Team } from "./
|
|
6
|
+
import { Team } from "./team.js";
|
|
7
7
|
import {
|
|
8
8
|
ReadableStream,
|
|
9
9
|
WritableStream,
|
|
@@ -86,7 +86,7 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
|
|
|
86
86
|
.transactions[0]!.madeAt,
|
|
87
87
|
changes: [
|
|
88
88
|
{
|
|
89
|
-
op: "
|
|
89
|
+
op: "set",
|
|
90
90
|
key: "hello",
|
|
91
91
|
value: "world",
|
|
92
92
|
} satisfies MapOpPayload<string, string>,
|
|
@@ -164,7 +164,7 @@ test("Node replies with only new tx to subscribe with some known state", async (
|
|
|
164
164
|
.transactions[1]!.madeAt,
|
|
165
165
|
changes: [
|
|
166
166
|
{
|
|
167
|
-
op: "
|
|
167
|
+
op: "set",
|
|
168
168
|
key: "goodbye",
|
|
169
169
|
value: "world",
|
|
170
170
|
} satisfies MapOpPayload<string, string>,
|
|
@@ -253,7 +253,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
|
|
253
253
|
.transactions[0]!.madeAt,
|
|
254
254
|
changes: [
|
|
255
255
|
{
|
|
256
|
-
op: "
|
|
256
|
+
op: "set",
|
|
257
257
|
key: "hello",
|
|
258
258
|
value: "world",
|
|
259
259
|
} satisfies MapOpPayload<string, string>,
|
|
@@ -285,7 +285,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
|
|
285
285
|
.transactions[1]!.madeAt,
|
|
286
286
|
changes: [
|
|
287
287
|
{
|
|
288
|
-
op: "
|
|
288
|
+
op: "set",
|
|
289
289
|
key: "goodbye",
|
|
290
290
|
value: "world",
|
|
291
291
|
} satisfies MapOpPayload<string, string>,
|
|
@@ -364,7 +364,7 @@ test("Client replies with known new content to tellKnownState from server", asyn
|
|
|
364
364
|
.transactions[0]!.madeAt,
|
|
365
365
|
changes: [
|
|
366
366
|
{
|
|
367
|
-
op: "
|
|
367
|
+
op: "set",
|
|
368
368
|
key: "hello",
|
|
369
369
|
value: "world",
|
|
370
370
|
} satisfies MapOpPayload<string, string>,
|
|
@@ -467,7 +467,7 @@ test("No matter the optimistic known state, node respects invalid known state me
|
|
|
467
467
|
.transactions[1]!.madeAt,
|
|
468
468
|
changes: [
|
|
469
469
|
{
|
|
470
|
-
op: "
|
|
470
|
+
op: "set",
|
|
471
471
|
key: "goodbye",
|
|
472
472
|
value: "world",
|
|
473
473
|
} satisfies MapOpPayload<string, string>,
|
|
@@ -570,7 +570,7 @@ test("If we add a server peer, all updates to all coValues are sent to it, even
|
|
|
570
570
|
.transactions[0]!.madeAt,
|
|
571
571
|
changes: [
|
|
572
572
|
{
|
|
573
|
-
op: "
|
|
573
|
+
op: "set",
|
|
574
574
|
key: "hello",
|
|
575
575
|
value: "world",
|
|
576
576
|
} satisfies MapOpPayload<string, string>,
|
package/src/team.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { CoID, ContentType } from "./contentType.js";
|
|
2
|
+
import { CoMap } from "./contentTypes/coMap.js";
|
|
3
|
+
import { JsonObject, JsonValue } from "./jsonValue.js";
|
|
4
|
+
import {
|
|
5
|
+
Encrypted,
|
|
6
|
+
KeyID,
|
|
7
|
+
KeySecret,
|
|
8
|
+
createdNowUnique,
|
|
9
|
+
newRandomKeySecret,
|
|
10
|
+
seal,
|
|
11
|
+
encryptKeySecret,
|
|
12
|
+
getAgentSealerID,
|
|
13
|
+
Sealed,
|
|
14
|
+
newRandomSecretSeed,
|
|
15
|
+
agentSecretFromSecretSeed,
|
|
16
|
+
getAgentID,
|
|
17
|
+
} from "./crypto.js";
|
|
18
|
+
import { LocalNode } from "./node.js";
|
|
19
|
+
import { SessionID, isAgentID } from "./ids.js";
|
|
20
|
+
import {
|
|
21
|
+
AccountIDOrAgentID,
|
|
22
|
+
GeneralizedControlledAccount,
|
|
23
|
+
Profile,
|
|
24
|
+
} from "./account.js";
|
|
25
|
+
import { Role } from "./permissions.js";
|
|
26
|
+
import { base58 } from "@scure/base";
|
|
27
|
+
|
|
28
|
+
export type TeamContent = {
|
|
29
|
+
profile: CoID<Profile> | null;
|
|
30
|
+
[key: AccountIDOrAgentID]: Role;
|
|
31
|
+
readKey: KeyID;
|
|
32
|
+
[revelationFor: `${KeyID}_for_${AccountIDOrAgentID}`]: Sealed<KeySecret>;
|
|
33
|
+
[oldKeyForNewKey: `${KeyID}_for_${KeyID}`]: Encrypted<
|
|
34
|
+
KeySecret,
|
|
35
|
+
{ encryptedID: KeyID; encryptingID: KeyID }
|
|
36
|
+
>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function expectTeamContent(
|
|
40
|
+
content: ContentType
|
|
41
|
+
): CoMap<TeamContent, JsonObject | null> {
|
|
42
|
+
if (content.type !== "comap") {
|
|
43
|
+
throw new Error("Expected map");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return content as CoMap<TeamContent, JsonObject | null>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class Team {
|
|
50
|
+
teamMap: CoMap<TeamContent, JsonObject | null>;
|
|
51
|
+
node: LocalNode;
|
|
52
|
+
|
|
53
|
+
constructor(
|
|
54
|
+
teamMap: CoMap<TeamContent, JsonObject | null>,
|
|
55
|
+
node: LocalNode
|
|
56
|
+
) {
|
|
57
|
+
this.teamMap = teamMap;
|
|
58
|
+
this.node = node;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get id(): CoID<CoMap<TeamContent, JsonObject | null>> {
|
|
62
|
+
return this.teamMap.id;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
roleOf(accountID: AccountIDOrAgentID): Role | undefined {
|
|
66
|
+
return this.teamMap.get(accountID);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
myRole(): Role | undefined {
|
|
70
|
+
return this.roleOf(this.node.account.id);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
addMember(accountID: AccountIDOrAgentID, role: Role) {
|
|
74
|
+
this.teamMap = this.teamMap.edit((map) => {
|
|
75
|
+
const currentReadKey = this.teamMap.coValue.getCurrentReadKey();
|
|
76
|
+
|
|
77
|
+
if (!currentReadKey.secret) {
|
|
78
|
+
throw new Error("Can't add member without read key secret");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const agent = this.node.resolveAccountAgent(
|
|
82
|
+
accountID,
|
|
83
|
+
"Expected to know agent to add them to team"
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
map.set(accountID, role, "trusting");
|
|
87
|
+
|
|
88
|
+
if (map.get(accountID) !== role) {
|
|
89
|
+
throw new Error("Failed to set role");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
map.set(
|
|
93
|
+
`${currentReadKey.id}_for_${accountID}`,
|
|
94
|
+
seal(
|
|
95
|
+
currentReadKey.secret,
|
|
96
|
+
this.teamMap.coValue.node.account.currentSealerSecret(),
|
|
97
|
+
getAgentSealerID(agent),
|
|
98
|
+
{
|
|
99
|
+
in: this.teamMap.coValue.id,
|
|
100
|
+
tx: this.teamMap.coValue.nextTransactionID(),
|
|
101
|
+
}
|
|
102
|
+
),
|
|
103
|
+
"trusting"
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
createInvite(role: "reader" | "writer" | "admin"): InviteSecret {
|
|
109
|
+
const secretSeed = newRandomSecretSeed();
|
|
110
|
+
|
|
111
|
+
const inviteSecret = agentSecretFromSecretSeed(secretSeed);
|
|
112
|
+
const inviteID = getAgentID(inviteSecret);
|
|
113
|
+
|
|
114
|
+
this.addMember(inviteID, `${role}Invite` as Role);
|
|
115
|
+
|
|
116
|
+
return inviteSecretFromSecretSeed(secretSeed);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
rotateReadKey() {
|
|
120
|
+
const currentlyPermittedReaders = this.teamMap.keys().filter((key) => {
|
|
121
|
+
if (key.startsWith("co_") || isAgentID(key)) {
|
|
122
|
+
const role = this.teamMap.get(key);
|
|
123
|
+
return (
|
|
124
|
+
role === "admin" || role === "writer" || role === "reader"
|
|
125
|
+
);
|
|
126
|
+
} else {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}) as AccountIDOrAgentID[];
|
|
130
|
+
|
|
131
|
+
const maybeCurrentReadKey = this.teamMap.coValue.getCurrentReadKey();
|
|
132
|
+
|
|
133
|
+
if (!maybeCurrentReadKey.secret) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"Can't rotate read key secret we don't have access to"
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const currentReadKey = {
|
|
140
|
+
id: maybeCurrentReadKey.id,
|
|
141
|
+
secret: maybeCurrentReadKey.secret,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const newReadKey = newRandomKeySecret();
|
|
145
|
+
|
|
146
|
+
this.teamMap = this.teamMap.edit((map) => {
|
|
147
|
+
for (const readerID of currentlyPermittedReaders) {
|
|
148
|
+
const reader = this.node.resolveAccountAgent(
|
|
149
|
+
readerID,
|
|
150
|
+
"Expected to know currently permitted reader"
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
map.set(
|
|
154
|
+
`${newReadKey.id}_for_${readerID}`,
|
|
155
|
+
seal(
|
|
156
|
+
newReadKey.secret,
|
|
157
|
+
this.teamMap.coValue.node.account.currentSealerSecret(),
|
|
158
|
+
getAgentSealerID(reader),
|
|
159
|
+
{
|
|
160
|
+
in: this.teamMap.coValue.id,
|
|
161
|
+
tx: this.teamMap.coValue.nextTransactionID(),
|
|
162
|
+
}
|
|
163
|
+
),
|
|
164
|
+
"trusting"
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
map.set(
|
|
169
|
+
`${currentReadKey.id}_for_${newReadKey.id}`,
|
|
170
|
+
encryptKeySecret({
|
|
171
|
+
encrypting: newReadKey,
|
|
172
|
+
toEncrypt: currentReadKey,
|
|
173
|
+
}).encrypted,
|
|
174
|
+
"trusting"
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
map.set("readKey", newReadKey.id, "trusting");
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
removeMember(accountID: AccountIDOrAgentID) {
|
|
182
|
+
this.teamMap = this.teamMap.edit((map) => {
|
|
183
|
+
map.set(accountID, "revoked", "trusting");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
this.rotateReadKey();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
createMap<
|
|
190
|
+
M extends { [key: string]: JsonValue },
|
|
191
|
+
Meta extends JsonObject | null = null
|
|
192
|
+
>(meta?: Meta): CoMap<M, Meta> {
|
|
193
|
+
return this.node
|
|
194
|
+
.createCoValue({
|
|
195
|
+
type: "comap",
|
|
196
|
+
ruleset: {
|
|
197
|
+
type: "ownedByTeam",
|
|
198
|
+
team: this.teamMap.id,
|
|
199
|
+
},
|
|
200
|
+
meta: meta || null,
|
|
201
|
+
...createdNowUnique(),
|
|
202
|
+
})
|
|
203
|
+
.getCurrentContent() as CoMap<M, Meta>;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
testWithDifferentAccount(
|
|
207
|
+
account: GeneralizedControlledAccount,
|
|
208
|
+
sessionId: SessionID
|
|
209
|
+
): Team {
|
|
210
|
+
return new Team(
|
|
211
|
+
expectTeamContent(
|
|
212
|
+
this.teamMap.coValue
|
|
213
|
+
.testWithDifferentAccount(account, sessionId)
|
|
214
|
+
.getCurrentContent()
|
|
215
|
+
),
|
|
216
|
+
this.node
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export type InviteSecret = `inviteSecret_z${string}`;
|
|
222
|
+
|
|
223
|
+
function inviteSecretFromSecretSeed(secretSeed: Uint8Array): InviteSecret {
|
|
224
|
+
return `inviteSecret_z${base58.encode(secretSeed)}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function secretSeedFromInviteSecret(inviteSecret: InviteSecret) {
|
|
228
|
+
if (!inviteSecret.startsWith("inviteSecret_z")) {
|
|
229
|
+
throw new Error("Invalid invite secret");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return base58.decode(inviteSecret.slice("inviteSecret_z".length));
|
|
233
|
+
}
|
package/src/testUtils.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AgentSecret, createdNowUnique, getAgentID, newRandomAgentSecret } from "./crypto.js";
|
|
2
2
|
import { newRandomSessionID } from "./coValue.js";
|
|
3
3
|
import { LocalNode } from "./node.js";
|
|
4
|
-
import { expectTeamContent } from "./
|
|
4
|
+
import { expectTeamContent } from "./team.js";
|
|
5
5
|
import { AnonymousControlledAccount } from "./account.js";
|
|
6
6
|
import { SessionID } from "./ids.js";
|
|
7
7
|
|
package/tsconfig.json
CHANGED
package/dist/account.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|