jazz-tools 0.9.23 → 0.10.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.
Files changed (45) hide show
  1. package/.turbo/turbo-build.log +11 -11
  2. package/CHANGELOG.md +19 -0
  3. package/dist/{chunk-OJIEP4WE.js → chunk-UBD75Z27.js} +566 -118
  4. package/dist/chunk-UBD75Z27.js.map +1 -0
  5. package/dist/index.native.js +17 -5
  6. package/dist/index.native.js.map +1 -1
  7. package/dist/index.web.js +17 -5
  8. package/dist/index.web.js.map +1 -1
  9. package/dist/testing.js +124 -33
  10. package/dist/testing.js.map +1 -1
  11. package/package.json +5 -3
  12. package/src/auth/AuthSecretStorage.ts +109 -0
  13. package/src/auth/DemoAuth.ts +188 -0
  14. package/src/auth/InMemoryKVStore.ts +25 -0
  15. package/src/auth/KvStoreContext.ts +39 -0
  16. package/src/auth/PassphraseAuth.ts +113 -0
  17. package/src/coValues/account.ts +8 -3
  18. package/src/coValues/coFeed.ts +1 -1
  19. package/src/coValues/coList.ts +1 -1
  20. package/src/coValues/coMap.ts +1 -1
  21. package/src/coValues/group.ts +9 -8
  22. package/src/coValues/interfaces.ts +14 -5
  23. package/src/exports.ts +17 -3
  24. package/src/implementation/ContextManager.ts +178 -0
  25. package/src/implementation/activeAccountContext.ts +6 -1
  26. package/src/implementation/createContext.ts +173 -149
  27. package/src/testing.ts +171 -33
  28. package/src/tests/AuthSecretStorage.test.ts +275 -0
  29. package/src/tests/ContextManager.test.ts +256 -0
  30. package/src/tests/DemoAuth.test.ts +269 -0
  31. package/src/tests/PassphraseAuth.test.ts +152 -0
  32. package/src/tests/coFeed.test.ts +44 -39
  33. package/src/tests/coList.test.ts +21 -20
  34. package/src/tests/coMap.test.ts +21 -20
  35. package/src/tests/coPlainText.test.ts +21 -20
  36. package/src/tests/coRichText.test.ts +21 -20
  37. package/src/tests/createContext.test.ts +339 -0
  38. package/src/tests/deepLoading.test.ts +41 -42
  39. package/src/tests/fixtures.ts +2050 -0
  40. package/src/tests/groupsAndAccounts.test.ts +2 -2
  41. package/src/tests/subscribe.test.ts +42 -9
  42. package/src/tests/testing.test.ts +56 -0
  43. package/src/tests/utils.ts +11 -11
  44. package/src/types.ts +54 -0
  45. package/dist/chunk-OJIEP4WE.js.map +0 -1
@@ -0,0 +1,178 @@
1
+ import { AgentSecret, LocalNode, cojsonInternals } from "cojson";
2
+ import { AuthSecretStorage } from "../auth/AuthSecretStorage.js";
3
+ import { InMemoryKVStore } from "../auth/InMemoryKVStore.js";
4
+ import { KvStore, KvStoreContext } from "../auth/KvStoreContext.js";
5
+ import { Account } from "../coValues/account.js";
6
+ import { AuthCredentials } from "../types.js";
7
+ import { JazzContextType } from "../types.js";
8
+ import { AnonymousJazzAgent } from "./anonymousJazzAgent.js";
9
+
10
+ export type JazzContextManagerAuthProps = {
11
+ credentials?: AuthCredentials;
12
+ newAccountProps?: { secret: AgentSecret; creationProps: { name: string } };
13
+ };
14
+
15
+ export type JazzContextManagerBaseProps<Acc extends Account> = {
16
+ onAnonymousAccountDiscarded?: (anonymousAccount: Acc) => Promise<void>;
17
+ onLogOut?: () => void;
18
+ };
19
+
20
+ type PlatformSpecificAuthContext<Acc extends Account> = {
21
+ me: Acc;
22
+ node: LocalNode;
23
+ logOut: () => Promise<void>;
24
+ done: () => void;
25
+ };
26
+
27
+ type PlatformSpecificGuestContext = {
28
+ guest: AnonymousJazzAgent;
29
+ node: LocalNode;
30
+ logOut: () => Promise<void>;
31
+ done: () => void;
32
+ };
33
+
34
+ type PlatformSpecificContext<Acc extends Account> =
35
+ | PlatformSpecificAuthContext<Acc>
36
+ | PlatformSpecificGuestContext;
37
+
38
+ export class JazzContextManager<
39
+ Acc extends Account,
40
+ P extends JazzContextManagerBaseProps<Acc>,
41
+ > {
42
+ protected value: JazzContextType<Acc> | undefined;
43
+ protected context: PlatformSpecificContext<Acc> | undefined;
44
+ protected props: P | undefined;
45
+ protected authSecretStorage = new AuthSecretStorage();
46
+ protected authenticating = false;
47
+
48
+ constructor() {
49
+ KvStoreContext.getInstance().initialize(this.getKvStore());
50
+ }
51
+
52
+ getKvStore(): KvStore {
53
+ return new InMemoryKVStore();
54
+ }
55
+
56
+ async createContext(props: P, authProps?: JazzContextManagerAuthProps) {
57
+ props;
58
+ authProps;
59
+ throw new Error("Not implemented");
60
+ }
61
+
62
+ updateContext(props: P, context: PlatformSpecificContext<Acc>) {
63
+ // When authenticating we don't want to close the previous context
64
+ // because we might need to handle the onAnonymousAccountDiscarded callback
65
+ if (!this.authenticating) {
66
+ this.context?.done();
67
+ }
68
+
69
+ this.context = context;
70
+ this.props = props;
71
+ this.value = {
72
+ ...context,
73
+ node: context.node,
74
+ authenticate: this.authenticate,
75
+ logOut: this.logOut,
76
+ };
77
+
78
+ this.notify();
79
+ }
80
+
81
+ propsChanged(props: P) {
82
+ props;
83
+ throw new Error("Not implemented");
84
+ }
85
+
86
+ getCurrentValue() {
87
+ return this.value;
88
+ }
89
+
90
+ getAuthSecretStorage() {
91
+ return this.authSecretStorage;
92
+ }
93
+
94
+ logOut = async () => {
95
+ if (!this.context || !this.props) {
96
+ return;
97
+ }
98
+
99
+ await this.context.logOut();
100
+ this.props.onLogOut?.();
101
+ return this.createContext(this.props);
102
+ };
103
+
104
+ done = () => {
105
+ if (!this.context) {
106
+ return;
107
+ }
108
+
109
+ this.context.done();
110
+ };
111
+
112
+ authenticate = async (credentials: AuthCredentials) => {
113
+ if (!this.props) {
114
+ throw new Error("Props required");
115
+ }
116
+
117
+ const prevContext = this.context;
118
+ const prevCredentials = await this.authSecretStorage.get();
119
+ const wasAnonymous =
120
+ this.authSecretStorage.getIsAuthenticated(prevCredentials) === false;
121
+
122
+ this.authenticating = true;
123
+ await this.createContext(this.props, { credentials }).finally(() => {
124
+ this.authenticating = false;
125
+ });
126
+
127
+ const currentContext = this.context;
128
+
129
+ if (
130
+ prevContext &&
131
+ currentContext &&
132
+ "me" in prevContext &&
133
+ "me" in currentContext &&
134
+ wasAnonymous
135
+ ) {
136
+ // Using a direct connection to make coValue transfer almost synchronous
137
+ const [prevAccountAsPeer, currentAccountAsPeer] =
138
+ cojsonInternals.connectedPeers(
139
+ prevContext.me.id,
140
+ currentContext.me.id,
141
+ {
142
+ peer1role: "client",
143
+ peer2role: "server",
144
+ },
145
+ );
146
+
147
+ prevContext.node.syncManager.addPeer(currentAccountAsPeer);
148
+ currentContext.node.syncManager.addPeer(prevAccountAsPeer);
149
+
150
+ try {
151
+ await this.props.onAnonymousAccountDiscarded?.(prevContext.me);
152
+ await prevContext.me.waitForAllCoValuesSync();
153
+ } catch (error) {
154
+ console.error("Error onAnonymousAccountDiscarded", error);
155
+ }
156
+
157
+ prevAccountAsPeer.outgoing.close();
158
+ currentAccountAsPeer.outgoing.close();
159
+ }
160
+
161
+ prevContext?.done();
162
+ };
163
+
164
+ listeners = new Set<() => void>();
165
+ subscribe = (callback: () => void) => {
166
+ this.listeners.add(callback);
167
+
168
+ return () => {
169
+ this.listeners.delete(callback);
170
+ };
171
+ };
172
+
173
+ notify() {
174
+ for (const listener of this.listeners) {
175
+ listener();
176
+ }
177
+ }
178
+ }
@@ -4,15 +4,20 @@ class ActiveAccountContext {
4
4
  private activeAccount: Account | null = null;
5
5
  private guestMode: boolean = false;
6
6
 
7
- set(account: Account) {
7
+ set(account: Account | null) {
8
8
  this.activeAccount = account;
9
9
  this.guestMode = false;
10
10
  }
11
11
 
12
12
  setGuestMode() {
13
+ this.activeAccount = null;
13
14
  this.guestMode = true;
14
15
  }
15
16
 
17
+ maybeGet() {
18
+ return this.activeAccount;
19
+ }
20
+
16
21
  get() {
17
22
  if (!this.activeAccount) {
18
23
  if (this.guestMode) {
@@ -9,9 +9,11 @@ import {
9
9
  RawAccountID,
10
10
  SessionID,
11
11
  } from "cojson";
12
+ import { AuthSecretStorage } from "../auth/AuthSecretStorage.js";
12
13
  import { type Account, type AccountClass } from "../coValues/account.js";
13
14
  import { RegisteredSchemas } from "../coValues/registeredSchemas.js";
14
15
  import type { ID } from "../internal.js";
16
+ import { AuthCredentials, NewAccountProps } from "../types.js";
15
17
  import { activeAccountContext } from "./activeAccountContext.js";
16
18
  import { AnonymousJazzAgent } from "./anonymousJazzAgent.js";
17
19
 
@@ -20,14 +22,20 @@ export type Credentials = {
20
22
  secret: AgentSecret;
21
23
  };
22
24
 
25
+ type SessionProvider = (
26
+ accountID: ID<Account>,
27
+ crypto: CryptoProvider,
28
+ ) => Promise<{ sessionID: SessionID; sessionDone: () => void }>;
29
+
23
30
  export type AuthResult =
24
31
  | {
25
32
  type: "existing";
33
+ username?: string;
26
34
  credentials: Credentials;
27
35
  saveCredentials?: (credentials: Credentials) => Promise<void>;
28
36
  onSuccess: () => void;
29
37
  onError: (error: string | Error) => void;
30
- logOut: () => void;
38
+ logOut: () => Promise<void>;
31
39
  }
32
40
  | {
33
41
  type: "new";
@@ -40,42 +48,9 @@ export type AuthResult =
40
48
  saveCredentials: (credentials: Credentials) => Promise<void>;
41
49
  onSuccess: () => void;
42
50
  onError: (error: string | Error) => void;
43
- logOut: () => void;
51
+ logOut: () => Promise<void>;
44
52
  };
45
53
 
46
- export interface AuthMethod {
47
- start(crypto: CryptoProvider): Promise<AuthResult>;
48
- }
49
-
50
- export const fixedCredentialsAuth = (credentials: {
51
- accountID: ID<Account>;
52
- secret: AgentSecret;
53
- }): AuthMethod => {
54
- return {
55
- start: async () => ({
56
- type: "existing",
57
- credentials,
58
- saveCredentials: async () => {},
59
- onSuccess: () => {},
60
- onError: () => {},
61
- logOut: () => {},
62
- }),
63
- };
64
- };
65
-
66
- export const ephemeralCredentialsAuth = (): AuthMethod => {
67
- return {
68
- start: async () => ({
69
- type: "new",
70
- creationProps: { name: "Ephemeral" },
71
- saveCredentials: async () => {},
72
- onSuccess: () => {},
73
- onError: () => {},
74
- logOut: () => {},
75
- }),
76
- };
77
- };
78
-
79
54
  export async function randomSessionProvider(
80
55
  accountID: ID<Account>,
81
56
  crypto: CryptoProvider,
@@ -86,152 +61,201 @@ export async function randomSessionProvider(
86
61
  };
87
62
  }
88
63
 
89
- type ContextParamsWithAuth<Acc extends Account> = {
90
- AccountSchema?: AccountClass<Acc>;
91
- auth: AuthMethod;
92
- sessionProvider: (
93
- accountID: ID<Account>,
94
- crypto: CryptoProvider,
95
- ) => Promise<{ sessionID: SessionID; sessionDone: () => void }>;
96
- } & BaseContextParams;
97
-
98
- type BaseContextParams = {
99
- peersToLoadFrom: Peer[];
100
- crypto: CryptoProvider;
101
- };
102
-
103
64
  export type JazzContextWithAccount<Acc extends Account> = {
65
+ node: LocalNode;
104
66
  account: Acc;
105
67
  done: () => void;
106
- logOut: () => void;
68
+ logOut: () => Promise<void>;
107
69
  };
108
70
 
109
71
  export type JazzContextWithAgent = {
110
72
  agent: AnonymousJazzAgent;
111
73
  done: () => void;
112
- logOut: () => void;
74
+ logOut: () => Promise<void>;
113
75
  };
114
76
 
115
77
  export type JazzContext<Acc extends Account> =
116
78
  | JazzContextWithAccount<Acc>
117
79
  | JazzContextWithAgent;
118
80
 
119
- export async function createJazzContext<Acc extends Account>({
120
- AccountSchema,
121
- auth,
122
- sessionProvider,
81
+ export async function createJazzContextFromExistingCredentials<
82
+ Acc extends Account,
83
+ >({
84
+ credentials,
123
85
  peersToLoadFrom,
124
86
  crypto,
125
- }: ContextParamsWithAuth<Acc>): Promise<JazzContextWithAccount<Acc>>;
126
- export async function createJazzContext({
87
+ AccountSchema: PropsAccountSchema,
88
+ sessionProvider,
89
+ onLogOut,
90
+ }: {
91
+ credentials: Credentials;
92
+ peersToLoadFrom: Peer[];
93
+ crypto: CryptoProvider;
94
+ AccountSchema?: AccountClass<Acc>;
95
+ sessionProvider: SessionProvider;
96
+ onLogOut?: () => void;
97
+ }): Promise<JazzContextWithAccount<Acc>> {
98
+ const { sessionID, sessionDone } = await sessionProvider(
99
+ credentials.accountID,
100
+ crypto,
101
+ );
102
+
103
+ const CurrentAccountSchema =
104
+ PropsAccountSchema ??
105
+ (RegisteredSchemas["Account"] as unknown as AccountClass<Acc>);
106
+
107
+ const node = await LocalNode.withLoadedAccount({
108
+ accountID: credentials.accountID as unknown as CoID<RawAccount>,
109
+ accountSecret: credentials.secret,
110
+ sessionID: sessionID,
111
+ peersToLoadFrom: peersToLoadFrom,
112
+ crypto: crypto,
113
+ });
114
+
115
+ const account = CurrentAccountSchema.fromNode(node);
116
+ activeAccountContext.set(account);
117
+
118
+ // Running the migration outside of withLoadedAccount for better error management
119
+ await account.applyMigration();
120
+
121
+ return {
122
+ node,
123
+ account,
124
+ done: () => {
125
+ node.gracefulShutdown();
126
+ sessionDone();
127
+ },
128
+ logOut: async () => {
129
+ node.gracefulShutdown();
130
+ sessionDone();
131
+ await onLogOut?.();
132
+ },
133
+ };
134
+ }
135
+
136
+ export async function createJazzContextForNewAccount<Acc extends Account>({
137
+ creationProps,
138
+ initialAgentSecret,
127
139
  peersToLoadFrom,
128
140
  crypto,
129
- }: BaseContextParams): Promise<JazzContextWithAgent>;
130
- export async function createJazzContext<Acc extends Account>(
131
- options: ContextParamsWithAuth<Acc> | BaseContextParams,
132
- ): Promise<JazzContext<Acc>>;
133
- export async function createJazzContext<Acc extends Account>(
134
- options: ContextParamsWithAuth<Acc> | BaseContextParams,
135
- ): Promise<JazzContext<Acc> | JazzContextWithAgent> {
136
- if (!("auth" in options)) {
137
- return createAnonymousJazzContext({
138
- peersToLoadFrom: options.peersToLoadFrom,
139
- crypto: options.crypto,
140
- });
141
- }
142
-
143
- const { auth, sessionProvider, peersToLoadFrom, crypto } = options;
144
- const AccountSchema =
145
- options.AccountSchema ??
141
+ AccountSchema: PropsAccountSchema,
142
+ onLogOut,
143
+ }: {
144
+ creationProps: { name: string };
145
+ initialAgentSecret?: AgentSecret;
146
+ peersToLoadFrom: Peer[];
147
+ crypto: CryptoProvider;
148
+ AccountSchema?: AccountClass<Acc>;
149
+ onLogOut?: () => Promise<void>;
150
+ }): Promise<JazzContextWithAccount<Acc>> {
151
+ const CurrentAccountSchema =
152
+ PropsAccountSchema ??
146
153
  (RegisteredSchemas["Account"] as unknown as AccountClass<Acc>);
147
- const authResult = await auth.start(crypto);
148
154
 
149
- if (authResult.type === "existing") {
150
- const { sessionID, sessionDone } = await sessionProvider(
151
- authResult.credentials.accountID,
152
- crypto,
153
- );
154
-
155
- const node = await LocalNode.withLoadedAccount({
156
- accountID: authResult.credentials
157
- .accountID as unknown as CoID<RawAccount>,
158
- accountSecret: authResult.credentials.secret,
159
- sessionID: sessionID,
160
- peersToLoadFrom: peersToLoadFrom,
161
- crypto: crypto,
162
- migration: async (rawAccount, _node, creationProps) => {
163
- const account = new AccountSchema({
164
- fromRaw: rawAccount,
165
- }) as Acc;
166
-
167
- activeAccountContext.set(account);
168
-
169
- await account.applyMigration(creationProps);
170
- },
171
- });
155
+ const { node } = await LocalNode.withNewlyCreatedAccount({
156
+ creationProps,
157
+ peersToLoadFrom,
158
+ crypto,
159
+ initialAgentSecret,
160
+ migration: async (rawAccount, _node, creationProps) => {
161
+ const account = new CurrentAccountSchema({
162
+ fromRaw: rawAccount,
163
+ }) as Acc;
164
+ activeAccountContext.set(account);
172
165
 
173
- const account = AccountSchema.fromNode(node);
174
- activeAccountContext.set(account);
166
+ await account.applyMigration(creationProps);
167
+ },
168
+ });
175
169
 
176
- if (authResult.saveCredentials) {
177
- await authResult.saveCredentials({
178
- accountID: node.account.id as unknown as ID<Account>,
179
- secret: node.account.agentSecret,
180
- });
181
- }
170
+ const account = CurrentAccountSchema.fromNode(node);
171
+ activeAccountContext.set(account);
172
+
173
+ return {
174
+ node,
175
+ account,
176
+ done: () => {
177
+ node.gracefulShutdown();
178
+ },
179
+ logOut: async () => {
180
+ node.gracefulShutdown();
181
+ await onLogOut?.();
182
+ },
183
+ };
184
+ }
182
185
 
183
- authResult.onSuccess();
186
+ export async function createJazzContext<Acc extends Account>(options: {
187
+ credentials?: AuthCredentials;
188
+ newAccountProps?: NewAccountProps;
189
+ peersToLoadFrom: Peer[];
190
+ crypto: CryptoProvider;
191
+ defaultProfileName?: string;
192
+ AccountSchema?: AccountClass<Acc>;
193
+ sessionProvider: SessionProvider;
194
+ authSecretStorage: AuthSecretStorage;
195
+ }) {
196
+ const crypto = options.crypto;
184
197
 
185
- return {
186
- account,
187
- done: () => {
188
- node.gracefulShutdown();
189
- sessionDone();
190
- },
191
- logOut: () => {
192
- node.gracefulShutdown();
193
- sessionDone();
194
- authResult.logOut();
198
+ let context: JazzContextWithAccount<Acc>;
199
+
200
+ const authSecretStorage = options.authSecretStorage;
201
+
202
+ await authSecretStorage.migrate();
203
+
204
+ const credentials = options.credentials ?? (await authSecretStorage.get());
205
+
206
+ if (credentials && !options.newAccountProps) {
207
+ context = await createJazzContextFromExistingCredentials({
208
+ credentials: {
209
+ accountID: credentials.accountID,
210
+ secret: credentials.accountSecret,
195
211
  },
196
- };
197
- } else if (authResult.type === "new") {
198
- const { node } = await LocalNode.withNewlyCreatedAccount({
199
- creationProps: authResult.creationProps,
200
- peersToLoadFrom: peersToLoadFrom,
201
- crypto: crypto,
202
- initialAgentSecret: authResult.initialSecret,
203
- migration: async (rawAccount, _node, creationProps) => {
204
- const account = new AccountSchema({
205
- fromRaw: rawAccount,
206
- }) as Acc;
207
- activeAccountContext.set(account);
208
-
209
- await account.applyMigration(creationProps);
212
+ peersToLoadFrom: options.peersToLoadFrom,
213
+ crypto,
214
+ AccountSchema: options.AccountSchema,
215
+ sessionProvider: options.sessionProvider,
216
+ onLogOut: () => {
217
+ authSecretStorage.clear();
210
218
  },
211
219
  });
212
220
 
213
- const account = AccountSchema.fromNode(node);
214
- activeAccountContext.set(account);
221
+ // To align the isAuthenticated state with the credentials
222
+ authSecretStorage.emitUpdate(credentials);
223
+ } else {
224
+ const secretSeed = options.crypto.newRandomSecretSeed();
215
225
 
216
- await authResult.saveCredentials({
217
- accountID: node.account.id as unknown as ID<Account>,
218
- secret: node.account.agentSecret,
219
- });
226
+ const initialAgentSecret =
227
+ options.newAccountProps?.secret ??
228
+ crypto.agentSecretFromSecretSeed(secretSeed);
220
229
 
221
- authResult.onSuccess();
222
- return {
223
- account,
224
- done: () => {
225
- node.gracefulShutdown();
226
- },
227
- logOut: () => {
228
- node.gracefulShutdown();
229
- authResult.logOut();
230
- },
230
+ const creationProps = options.newAccountProps?.creationProps ?? {
231
+ name: options.defaultProfileName ?? "Anonymous user",
231
232
  };
233
+
234
+ context = await createJazzContextForNewAccount({
235
+ creationProps,
236
+ initialAgentSecret,
237
+ peersToLoadFrom: options.peersToLoadFrom,
238
+ crypto,
239
+ AccountSchema: options.AccountSchema,
240
+ onLogOut: async () => {
241
+ await authSecretStorage.clear();
242
+ },
243
+ });
244
+
245
+ if (!options.newAccountProps) {
246
+ await authSecretStorage.set({
247
+ accountID: context.account.id,
248
+ secretSeed,
249
+ accountSecret: context.node.account.agentSecret,
250
+ provider: "anonymous",
251
+ });
252
+ }
232
253
  }
233
254
 
234
- throw new Error("Invalid auth result");
255
+ return {
256
+ ...context,
257
+ authSecretStorage,
258
+ };
235
259
  }
236
260
 
237
261
  export async function createAnonymousJazzContext({
@@ -259,6 +283,6 @@ export async function createAnonymousJazzContext({
259
283
  return {
260
284
  agent: new AnonymousJazzAgent(node),
261
285
  done: () => {},
262
- logOut: () => {},
286
+ logOut: async () => {},
263
287
  };
264
288
  }