cojson 0.0.21 → 0.0.22

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.
Files changed (55) hide show
  1. package/dist/account.d.ts +1 -1
  2. package/dist/account.js +1 -1
  3. package/dist/account.js.map +1 -1
  4. package/dist/coValue.d.ts +3 -2
  5. package/dist/coValue.js +10 -9
  6. package/dist/coValue.js.map +1 -1
  7. package/dist/contentTypes/coMap.d.ts +2 -2
  8. package/dist/contentTypes/coMap.js +6 -6
  9. package/dist/contentTypes/coMap.js.map +1 -1
  10. package/dist/index.d.ts +5 -2
  11. package/dist/index.js +3 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/node.d.ts +2 -1
  14. package/dist/node.js +43 -3
  15. package/dist/node.js.map +1 -1
  16. package/dist/permissions.d.ts +4 -32
  17. package/dist/permissions.js +42 -106
  18. package/dist/permissions.js.map +1 -1
  19. package/dist/team.d.ts +35 -0
  20. package/dist/team.js +110 -0
  21. package/dist/team.js.map +1 -0
  22. package/dist/testUtils.d.ts +2 -2
  23. package/dist/testUtils.js +1 -1
  24. package/dist/testUtils.js.map +1 -1
  25. package/package.json +2 -2
  26. package/src/account.ts +1 -1
  27. package/src/coValue.ts +10 -11
  28. package/src/contentTypes/coMap.ts +8 -8
  29. package/src/crypto.test.ts +10 -9
  30. package/src/index.ts +5 -0
  31. package/src/node.ts +113 -11
  32. package/src/permissions.test.ts +503 -3
  33. package/src/permissions.ts +70 -206
  34. package/src/sync.test.ts +8 -8
  35. package/src/team.ts +225 -0
  36. package/src/testUtils.ts +1 -1
  37. package/tsconfig.json +1 -0
  38. package/dist/account.test.d.ts +0 -1
  39. package/dist/account.test.js +0 -40
  40. package/dist/account.test.js.map +0 -1
  41. package/dist/coValue.test.d.ts +0 -1
  42. package/dist/coValue.test.js +0 -78
  43. package/dist/coValue.test.js.map +0 -1
  44. package/dist/contentType.test.d.ts +0 -1
  45. package/dist/contentType.test.js +0 -145
  46. package/dist/contentType.test.js.map +0 -1
  47. package/dist/crypto.test.d.ts +0 -1
  48. package/dist/crypto.test.js +0 -111
  49. package/dist/crypto.test.js.map +0 -1
  50. package/dist/permissions.test.d.ts +0 -1
  51. package/dist/permissions.test.js +0 -711
  52. package/dist/permissions.test.js.map +0 -1
  53. package/dist/sync.test.d.ts +0 -1
  54. package/dist/sync.test.js +0 -827
  55. package/dist/sync.test.js.map +0 -1
@@ -20,6 +20,7 @@ import { x25519 } from "@noble/curves/ed25519";
20
20
  import { xsalsa20_poly1305 } from "@noble/ciphers/salsa";
21
21
  import { blake3 } from "@noble/hashes/blake3";
22
22
  import stableStringify from "fast-json-stable-stringify";
23
+ import { SessionID } from './ids.js';
23
24
 
24
25
  test("Signatures round-trip and use stable stringify", () => {
25
26
  const data = { b: "world", a: "hello" };
@@ -49,7 +50,7 @@ test("encrypting round-trips, but invalid receiver can't unseal", () => {
49
50
 
50
51
  const nOnceMaterial = {
51
52
  in: "co_zTEST",
52
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 0 },
53
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
53
54
  } as const;
54
55
 
55
56
  const sealed = seal(
@@ -101,22 +102,22 @@ test("Encryption for transactions round-trips", () => {
101
102
 
102
103
  const encrypted1 = encryptForTransaction({ a: "hello" }, secret, {
103
104
  in: "co_zTEST",
104
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 0 },
105
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
105
106
  });
106
107
 
107
108
  const encrypted2 = encryptForTransaction({ b: "world" }, secret, {
108
109
  in: "co_zTEST",
109
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 1 },
110
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 1 },
110
111
  });
111
112
 
112
113
  const decrypted1 = decryptForTransaction(encrypted1, secret, {
113
114
  in: "co_zTEST",
114
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 0 },
115
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
115
116
  });
116
117
 
117
118
  const decrypted2 = decryptForTransaction(encrypted2, secret, {
118
119
  in: "co_zTEST",
119
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 1 },
120
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 1 },
120
121
  });
121
122
 
122
123
  expect([decrypted1, decrypted2]).toEqual([{ a: "hello" }, { b: "world" }]);
@@ -128,22 +129,22 @@ test("Encryption for transactions doesn't decrypt with a wrong key", () => {
128
129
 
129
130
  const encrypted1 = encryptForTransaction({ a: "hello" }, secret, {
130
131
  in: "co_zTEST",
131
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 0 },
132
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
132
133
  });
133
134
 
134
135
  const encrypted2 = encryptForTransaction({ b: "world" }, secret, {
135
136
  in: "co_zTEST",
136
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 1 },
137
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 1 },
137
138
  });
138
139
 
139
140
  const decrypted1 = decryptForTransaction(encrypted1, secret2, {
140
141
  in: "co_zTEST",
141
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 0 },
142
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
142
143
  });
143
144
 
144
145
  const decrypted2 = decryptForTransaction(encrypted2, secret2, {
145
146
  in: "co_zTEST",
146
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 1 },
147
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 1 },
147
148
  });
148
149
 
149
150
  expect([decrypted1, decrypted2]).toEqual([undefined, undefined]);
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  import { connectedPeers } from "./streamUtils.js";
15
15
  import { AnonymousControlledAccount, ControlledAccount } from "./account.js";
16
16
  import { rawCoIDtoBytes, rawCoIDfromBytes } from "./ids.js";
17
+ import { Team, expectTeamContent } from "./team.js"
17
18
 
18
19
  import type { SessionID, AgentID } from "./ids.js";
19
20
  import type { CoID, ContentType } from "./contentType.js";
@@ -26,6 +27,7 @@ import type {
26
27
  ProfileContent,
27
28
  Profile,
28
29
  } from "./account.js";
30
+ import type { InviteSecret } from "./team.js";
29
31
 
30
32
  type Value = JsonValue | ContentType;
31
33
 
@@ -42,6 +44,7 @@ export const cojsonInternals = {
42
44
  agentSecretFromSecretSeed,
43
45
  secretSeedLength,
44
46
  shortHashLength,
47
+ expectTeamContent
45
48
  };
46
49
 
47
50
  export {
@@ -50,6 +53,7 @@ export {
50
53
  CoMap,
51
54
  AnonymousControlledAccount,
52
55
  ControlledAccount,
56
+ Team
53
57
  };
54
58
 
55
59
  export type {
@@ -66,6 +70,7 @@ export type {
66
70
  AccountContent,
67
71
  Profile,
68
72
  ProfileContent,
73
+ InviteSecret
69
74
  };
70
75
 
71
76
  // eslint-disable-next-line @typescript-eslint/no-namespace
package/src/node.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  AgentSecret,
3
+ agentSecretFromSecretSeed,
3
4
  createdNowUnique,
4
5
  getAgentID,
5
6
  getAgentSealerID,
@@ -9,7 +10,13 @@ import {
9
10
  seal,
10
11
  } from "./crypto.js";
11
12
  import { CoValue, CoValueHeader, newRandomSessionID } from "./coValue.js";
12
- import { Team, TeamContent, expectTeamContent } from "./permissions.js";
13
+ import {
14
+ InviteSecret,
15
+ Team,
16
+ TeamContent,
17
+ expectTeamContent,
18
+ secretSeedFromInviteSecret,
19
+ } from "./team.js";
13
20
  import { Peer, SyncManager } from "./sync.js";
14
21
  import { AgentID, RawCoID, SessionID, isAgentID } from "./ids.js";
15
22
  import { CoID, ContentType } from "./contentType.js";
@@ -43,7 +50,10 @@ export class LocalNode {
43
50
  this.ownSessionID = ownSessionID;
44
51
  }
45
52
 
46
- static withNewlyCreatedAccount(name: string, initialAgentSecret = newRandomAgentSecret()): {
53
+ static withNewlyCreatedAccount(
54
+ name: string,
55
+ initialAgentSecret = newRandomAgentSecret()
56
+ ): {
47
57
  node: LocalNode;
48
58
  accountID: AccountID;
49
59
  accountSecret: AgentSecret;
@@ -70,8 +80,16 @@ export class LocalNode {
70
80
  };
71
81
  }
72
82
 
73
- static async withLoadedAccount(accountID: AccountID, accountSecret: AgentSecret, sessionID: SessionID, peersToLoadFrom: Peer[]): Promise<LocalNode> {
74
- const loadingNode = new LocalNode(new AnonymousControlledAccount(accountSecret), newRandomSessionID(accountID));
83
+ static async withLoadedAccount(
84
+ accountID: AccountID,
85
+ accountSecret: AgentSecret,
86
+ sessionID: SessionID,
87
+ peersToLoadFrom: Peer[]
88
+ ): Promise<LocalNode> {
89
+ const loadingNode = new LocalNode(
90
+ new AnonymousControlledAccount(accountSecret),
91
+ newRandomSessionID(accountID)
92
+ );
75
93
 
76
94
  const accountPromise = loadingNode.load(accountID);
77
95
 
@@ -82,7 +100,10 @@ export class LocalNode {
82
100
  const account = await accountPromise;
83
101
 
84
102
  // since this is all synchronous, we can just swap out nodes for the SyncManager
85
- const node = loadingNode.testWithDifferentAccount(new ControlledAccount(accountSecret, account, loadingNode), sessionID);
103
+ const node = loadingNode.testWithDifferentAccount(
104
+ new ControlledAccount(accountSecret, account, loadingNode),
105
+ sessionID
106
+ );
86
107
  node.sync = loadingNode.sync;
87
108
  node.sync.local = node;
88
109
 
@@ -124,7 +145,75 @@ export class LocalNode {
124
145
  if (!profileID) {
125
146
  throw new Error(`Account ${id} has no profile`);
126
147
  }
127
- return (await this.loadCoValue(profileID)).getCurrentContent() as Profile;
148
+ return (
149
+ await this.loadCoValue(profileID)
150
+ ).getCurrentContent() as Profile;
151
+ }
152
+
153
+ async acceptInvite<T extends ContentType>(
154
+ teamOrOwnedValueID: CoID<T>,
155
+ inviteSecret: InviteSecret
156
+ ): Promise<void> {
157
+ const teamOrOwnedValue = await this.load(teamOrOwnedValueID);
158
+
159
+ if (teamOrOwnedValue.coValue.header.ruleset.type === "ownedByTeam") {
160
+ return this.acceptInvite(
161
+ teamOrOwnedValue.coValue.header.ruleset.team as CoID<
162
+ CoMap<TeamContent>
163
+ >,
164
+ inviteSecret
165
+ );
166
+ } else if (teamOrOwnedValue.coValue.header.ruleset.type !== "team") {
167
+ throw new Error("Can only accept invites to teams");
168
+ }
169
+
170
+ const team = new Team(expectTeamContent(teamOrOwnedValue), this);
171
+
172
+ const inviteAgentSecret = agentSecretFromSecretSeed(
173
+ secretSeedFromInviteSecret(inviteSecret)
174
+ );
175
+ const inviteAgentID = getAgentID(inviteAgentSecret);
176
+
177
+ const invitationRole = await new Promise((resolve, reject) => {
178
+ team.teamMap.subscribe((teamMap) => {
179
+ const role = teamMap.get(inviteAgentID);
180
+ if (role) {
181
+ resolve(role);
182
+ }
183
+ });
184
+ setTimeout(() => reject(new Error("Couldn't find invitation before timeout")), 1000);
185
+ });
186
+
187
+ if (!invitationRole) {
188
+ throw new Error("No invitation found");
189
+ }
190
+
191
+ const existingRole = team.teamMap.get(this.account.id);
192
+
193
+ if (
194
+ existingRole === "admin" ||
195
+ (existingRole === "writer" && invitationRole === "reader")
196
+ ) {
197
+ console.debug("Not accepting invite that would downgrade role");
198
+ return;
199
+ }
200
+
201
+ const teamAsInvite = team.testWithDifferentAccount(
202
+ new AnonymousControlledAccount(inviteAgentSecret),
203
+ newRandomSessionID(inviteAgentID)
204
+ );
205
+
206
+ teamAsInvite.addMember(
207
+ this.account.id,
208
+ invitationRole === "adminInvite"
209
+ ? "admin"
210
+ : invitationRole === "writerInvite"
211
+ ? "writer"
212
+ : "reader"
213
+ );
214
+
215
+ team.teamMap.coValue.sessions = teamAsInvite.teamMap.coValue.sessions;
216
+ team.teamMap.coValue._cachedContent = undefined;
128
217
  }
129
218
 
130
219
  expectCoValueLoaded(id: RawCoID, expectation?: string): CoValue {
@@ -146,7 +235,9 @@ export class LocalNode {
146
235
 
147
236
  expectProfileLoaded(id: AccountID, expectation?: string): Profile {
148
237
  const account = this.expectCoValueLoaded(id, expectation);
149
- const profileID = expectTeamContent(account.getCurrentContent()).get("profile");
238
+ const profileID = expectTeamContent(account.getCurrentContent()).get(
239
+ "profile"
240
+ );
150
241
  if (!profileID) {
151
242
  throw new Error(
152
243
  `${
@@ -154,10 +245,16 @@ export class LocalNode {
154
245
  }Account ${id} has no profile`
155
246
  );
156
247
  }
157
- return this.expectCoValueLoaded(profileID, expectation).getCurrentContent() as Profile;
248
+ return this.expectCoValueLoaded(
249
+ profileID,
250
+ expectation
251
+ ).getCurrentContent() as Profile;
158
252
  }
159
253
 
160
- createAccount(name: string, agentSecret = newRandomAgentSecret()): ControlledAccount {
254
+ createAccount(
255
+ name: string,
256
+ agentSecret = newRandomAgentSecret()
257
+ ): ControlledAccount {
161
258
  const account = this.createCoValue(
162
259
  accountHeaderForInitialAgentSecret(agentSecret)
163
260
  ).testWithDifferentAccount(
@@ -165,7 +262,10 @@ export class LocalNode {
165
262
  newRandomSessionID(getAgentID(agentSecret))
166
263
  );
167
264
 
168
- const accountAsTeam = new Team(expectTeamContent(account.getCurrentContent()), account.node);
265
+ const accountAsTeam = new Team(
266
+ expectTeamContent(account.getCurrentContent()),
267
+ account.node
268
+ );
169
269
 
170
270
  accountAsTeam.teamMap.edit((editable) => {
171
271
  editable.set(getAgentID(agentSecret), "admin", "trusting");
@@ -195,7 +295,9 @@ export class LocalNode {
195
295
  account.node
196
296
  );
197
297
 
198
- const profile = accountAsTeam.createMap<ProfileContent, ProfileMeta>({ type: "profile" });
298
+ const profile = accountAsTeam.createMap<ProfileContent, ProfileMeta>({
299
+ type: "profile",
300
+ });
199
301
 
200
302
  profile.edit((editable) => {
201
303
  editable.set("name", name, "trusting");