jazz-tools 0.10.12 → 0.10.13

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/index.js CHANGED
@@ -35,7 +35,7 @@ import {
35
35
  parseInviteLink,
36
36
  randomSessionProvider,
37
37
  subscribeToCoValue
38
- } from "./chunk-RL7HVQ5Q.js";
38
+ } from "./chunk-GSIV52ZH.js";
39
39
 
40
40
  // src/index.ts
41
41
  import { MAX_RECOMMENDED_TX_SIZE, cojsonInternals } from "cojson";
package/dist/testing.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  createAnonymousJazzContext,
6
6
  createJazzContext,
7
7
  randomSessionProvider
8
- } from "./chunk-RL7HVQ5Q.js";
8
+ } from "./chunk-GSIV52ZH.js";
9
9
 
10
10
  // src/testing.ts
11
11
  import { LocalNode } from "cojson";
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "type": "module",
19
19
  "license": "MIT",
20
- "version": "0.10.12",
20
+ "version": "0.10.13",
21
21
  "dependencies": {
22
22
  "@scure/bip39": "^1.3.0",
23
23
  "cojson": "0.10.8"
@@ -3,7 +3,10 @@ import { entropyToMnemonic } from "@scure/bip39";
3
3
  import { CryptoProvider, cojsonInternals } from "cojson";
4
4
  import { Account } from "../coValues/account.js";
5
5
  import type { ID } from "../internal.js";
6
- import type { AuthenticateAccountFunction } from "../types.js";
6
+ import type {
7
+ AuthenticateAccountFunction,
8
+ RegisterAccountFunction,
9
+ } from "../types.js";
7
10
  import { AuthSecretStorage } from "./AuthSecretStorage.js";
8
11
 
9
12
  /**
@@ -23,6 +26,7 @@ export class PassphraseAuth {
23
26
  constructor(
24
27
  private crypto: CryptoProvider,
25
28
  private authenticate: AuthenticateAccountFunction,
29
+ private register: RegisterAccountFunction,
26
30
  private authSecretStorage: AuthSecretStorage,
27
31
  public wordlist: string[],
28
32
  ) {}
@@ -61,7 +65,7 @@ export class PassphraseAuth {
61
65
  this.notify();
62
66
  };
63
67
 
64
- signUp = async () => {
68
+ signUp = async (name?: string) => {
65
69
  const credentials = await this.authSecretStorage.get();
66
70
 
67
71
  if (!credentials || !credentials.secretSeed) {
@@ -77,9 +81,32 @@ export class PassphraseAuth {
77
81
  provider: "passphrase",
78
82
  });
79
83
 
84
+ if (name?.trim()) {
85
+ const currentAccount = await Account.getMe().ensureLoaded({
86
+ profile: {},
87
+ });
88
+
89
+ currentAccount.profile.name = name;
90
+ }
91
+
80
92
  return passphrase;
81
93
  };
82
94
 
95
+ registerNewAccount = async (passphrase: string, name: string) => {
96
+ const secretSeed = bip39.mnemonicToEntropy(passphrase, this.wordlist);
97
+ const accountSecret = this.crypto.agentSecretFromSecretSeed(secretSeed);
98
+ const accountID = await this.register(accountSecret, { name });
99
+
100
+ await this.authSecretStorage.set({
101
+ accountID,
102
+ secretSeed,
103
+ accountSecret,
104
+ provider: "passphrase",
105
+ });
106
+
107
+ return accountID;
108
+ };
109
+
83
110
  getCurrentAccountPassphrase = async () => {
84
111
  const credentials = await this.authSecretStorage.get();
85
112
 
@@ -90,6 +117,10 @@ export class PassphraseAuth {
90
117
  return entropyToMnemonic(credentials.secretSeed, this.wordlist);
91
118
  };
92
119
 
120
+ generateRandomPassphrase = () => {
121
+ return entropyToMnemonic(this.crypto.newRandomSecretSeed(), this.wordlist);
122
+ };
123
+
93
124
  loadCurrentAccountPassphrase = async () => {
94
125
  const passphrase = await this.getCurrentAccountPassphrase();
95
126
  this.passphrase = passphrase;
@@ -80,6 +80,7 @@ export class JazzContextManager<
80
80
  ...context,
81
81
  node: context.node,
82
82
  authenticate: this.authenticate,
83
+ register: this.register,
83
84
  logOut: this.logOut,
84
85
  };
85
86
 
@@ -143,14 +144,59 @@ export class JazzContextManager<
143
144
  this.authenticating = false;
144
145
  });
145
146
 
147
+ if (wasAnonymous) {
148
+ await this.handleAnonymousAccountMigration(prevContext);
149
+ }
150
+ };
151
+
152
+ register = async (
153
+ accountSecret: AgentSecret,
154
+ creationProps: { name: string },
155
+ ) => {
156
+ if (!this.props) {
157
+ throw new Error("Props required");
158
+ }
159
+
160
+ const prevContext = this.context;
161
+ const prevCredentials = await this.authSecretStorage.get();
162
+ const wasAnonymous =
163
+ this.authSecretStorage.getIsAuthenticated(prevCredentials) === false;
164
+
165
+ this.authenticating = true;
166
+ await this.createContext(this.props, {
167
+ newAccountProps: {
168
+ secret: accountSecret,
169
+ creationProps,
170
+ },
171
+ }).finally(() => {
172
+ this.authenticating = false;
173
+ });
174
+
175
+ if (wasAnonymous) {
176
+ await this.handleAnonymousAccountMigration(prevContext);
177
+ }
178
+
179
+ if (this.context && "me" in this.context) {
180
+ return this.context.me.id;
181
+ }
182
+
183
+ throw new Error("The registration hasn't created a new account");
184
+ };
185
+
186
+ private async handleAnonymousAccountMigration(
187
+ prevContext: PlatformSpecificContext<Acc> | undefined,
188
+ ) {
189
+ if (!this.props) {
190
+ throw new Error("Props required");
191
+ }
192
+
146
193
  const currentContext = this.context;
147
194
 
148
195
  if (
149
196
  prevContext &&
150
197
  currentContext &&
151
198
  "me" in prevContext &&
152
- "me" in currentContext &&
153
- wasAnonymous
199
+ "me" in currentContext
154
200
  ) {
155
201
  // Using a direct connection to make coValue transfer almost synchronous
156
202
  const [prevAccountAsPeer, currentAccountAsPeer] =
@@ -178,7 +224,7 @@ export class JazzContextManager<
178
224
  }
179
225
 
180
226
  prevContext?.done();
181
- };
227
+ }
182
228
 
183
229
  listeners = new Set<() => void>();
184
230
  subscribe = (callback: () => void) => {
@@ -338,4 +338,53 @@ describe("ContextManager", () => {
338
338
 
339
339
  expect(me.root.transferredRoot?.value).toBe("Hello");
340
340
  });
341
+
342
+ test("handles registration of new account", async () => {
343
+ const onAnonymousAccountDiscarded = vi.fn();
344
+ await manager.createContext({ onAnonymousAccountDiscarded });
345
+
346
+ const secret = Crypto.newRandomAgentSecret();
347
+ const accountId = await manager.register(secret, { name: "Test User" });
348
+
349
+ expect(accountId).toBeDefined();
350
+ const context = getCurrentValue();
351
+ expect(context.me.profile?.name).toBe("Test User");
352
+ expect(context.me.id).toBe(accountId);
353
+ });
354
+
355
+ test("calls onAnonymousAccountDiscarded when registering from anonymous user", async () => {
356
+ const onAnonymousAccountDiscarded = vi.fn();
357
+ await manager.createContext({ onAnonymousAccountDiscarded });
358
+ const anonymousAccount = getCurrentValue().me;
359
+
360
+ const secret = Crypto.newRandomAgentSecret();
361
+ await manager.register(secret, { name: "Test User" });
362
+
363
+ expect(onAnonymousAccountDiscarded).toHaveBeenCalledWith(anonymousAccount);
364
+ });
365
+
366
+ test("does not call onAnonymousAccountDiscarded when registering from authenticated user", async () => {
367
+ const onAnonymousAccountDiscarded = vi.fn();
368
+ const account = await createJazzTestAccount();
369
+
370
+ await manager.getAuthSecretStorage().set({
371
+ accountID: account.id,
372
+ accountSecret: account._raw.core.node.account.agentSecret,
373
+ provider: "test",
374
+ });
375
+
376
+ await manager.createContext({ onAnonymousAccountDiscarded });
377
+
378
+ const secret = Crypto.newRandomAgentSecret();
379
+ await manager.register(secret, { name: "New User" });
380
+
381
+ expect(onAnonymousAccountDiscarded).not.toHaveBeenCalled();
382
+ });
383
+
384
+ test("throws error when registering without props", async () => {
385
+ const secret = Crypto.newRandomAgentSecret();
386
+ await expect(
387
+ manager.register(secret, { name: "Test User" }),
388
+ ).rejects.toThrow("Props required");
389
+ });
341
390
  });
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { mnemonicToEntropy } from "@scure/bip39";
4
4
  import { AgentSecret } from "cojson";
5
+ import { PureJSCrypto } from "cojson/src/crypto/PureJSCrypto";
5
6
  import {
6
7
  Account,
7
8
  AuthSecretStorage,
@@ -9,31 +10,41 @@ import {
9
10
  InMemoryKVStore,
10
11
  KvStoreContext,
11
12
  } from "jazz-tools";
12
- import { beforeEach, describe, expect, it, vi } from "vitest";
13
+ import { assert, beforeEach, describe, expect, it, vi } from "vitest";
13
14
  import { PassphraseAuth } from "../auth/PassphraseAuth";
14
- import { createJazzTestAccount } from "../testing";
15
- import { TestJSCrypto } from "../testing";
15
+ import {
16
+ TestJazzContextManager,
17
+ createJazzTestAccount,
18
+ setupJazzTestSync,
19
+ } from "../testing";
16
20
  import { testWordlist } from "./fixtures";
17
21
 
18
22
  // Initialize KV store for tests
19
23
  KvStoreContext.getInstance().initialize(new InMemoryKVStore());
20
24
 
25
+ beforeEach(async () => {
26
+ await setupJazzTestSync();
27
+ });
28
+
21
29
  describe("PassphraseAuth", () => {
22
- let crypto: TestJSCrypto;
30
+ let crypto: PureJSCrypto;
23
31
  let mockAuthenticate: any;
32
+ let mockRegister: any;
24
33
  let authSecretStorage: AuthSecretStorage;
25
34
  let passphraseAuth: PassphraseAuth;
35
+ let account: Account;
26
36
 
27
37
  beforeEach(async () => {
28
38
  // Reset storage
29
39
  KvStoreContext.getInstance().getStorage().clearAll();
30
40
 
31
41
  // Set up crypto and mocks
32
- crypto = await TestJSCrypto.create();
42
+ crypto = await PureJSCrypto.create();
33
43
  mockAuthenticate = vi.fn();
44
+ mockRegister = vi.fn();
34
45
  authSecretStorage = new AuthSecretStorage();
35
46
 
36
- await createJazzTestAccount({
47
+ account = await createJazzTestAccount({
37
48
  isCurrentActiveAccount: true,
38
49
  });
39
50
 
@@ -41,6 +52,7 @@ describe("PassphraseAuth", () => {
41
52
  passphraseAuth = new PassphraseAuth(
42
53
  crypto,
43
54
  mockAuthenticate,
55
+ mockRegister,
44
56
  authSecretStorage,
45
57
  testWordlist,
46
58
  );
@@ -121,6 +133,39 @@ describe("PassphraseAuth", () => {
121
133
  "No credentials found",
122
134
  );
123
135
  });
136
+
137
+ it("should set account name when provided during signup", async () => {
138
+ const storageData = {
139
+ accountID: "test-account-id" as ID<Account>,
140
+ accountSecret: "test-secret" as AgentSecret,
141
+ secretSeed: new Uint8Array([
142
+ 173, 58, 235, 40, 67, 188, 236, 11, 107, 237, 97, 23, 182, 49, 188,
143
+ 63, 237, 52, 27, 84, 142, 66, 244, 149, 243, 114, 203, 164, 115, 239,
144
+ 175, 194,
145
+ ]),
146
+ provider: "anonymous",
147
+ };
148
+
149
+ await authSecretStorage.set(storageData);
150
+
151
+ const testName = "Test User";
152
+ await passphraseAuth.signUp(testName);
153
+
154
+ // Verify the account name was set
155
+ const { profile } = await account.ensureLoaded({
156
+ profile: {},
157
+ });
158
+ expect(profile.name).toBe(testName);
159
+
160
+ // Verify storage was updated correctly
161
+ const storedData = await authSecretStorage.get();
162
+ expect(storedData).toEqual({
163
+ accountID: storageData.accountID,
164
+ accountSecret: storageData.accountSecret,
165
+ secretSeed: storageData.secretSeed,
166
+ provider: "passphrase",
167
+ });
168
+ });
124
169
  });
125
170
 
126
171
  describe("getCurrentAccountPassphrase", () => {
@@ -150,3 +195,153 @@ describe("PassphraseAuth", () => {
150
195
  });
151
196
  });
152
197
  });
198
+
199
+ // Initialize KV store for tests
200
+ KvStoreContext.getInstance().initialize(new InMemoryKVStore());
201
+
202
+ describe("PassphraseAuth with TestJazzContextManager", () => {
203
+ let crypto: PureJSCrypto;
204
+ let contextManager: TestJazzContextManager<any>;
205
+ let authSecretStorage: AuthSecretStorage;
206
+ let passphraseAuth: PassphraseAuth;
207
+
208
+ beforeEach(async () => {
209
+ // Reset storage
210
+ KvStoreContext.getInstance().getStorage().clearAll();
211
+
212
+ const account = await createJazzTestAccount({
213
+ isCurrentActiveAccount: true,
214
+ });
215
+
216
+ // Set up crypto and context manager
217
+ crypto = await PureJSCrypto.create();
218
+ contextManager = TestJazzContextManager.fromAccountOrGuest(account);
219
+ authSecretStorage = contextManager.getAuthSecretStorage();
220
+
221
+ // Create initial context
222
+ await contextManager.createContext({});
223
+
224
+ // Create PassphraseAuth instance
225
+ passphraseAuth = new PassphraseAuth(
226
+ crypto,
227
+ contextManager.authenticate,
228
+ contextManager.register,
229
+ authSecretStorage,
230
+ testWordlist,
231
+ );
232
+ });
233
+
234
+ describe("logIn", () => {
235
+ it("should successfully log in with valid passphrase", async () => {
236
+ // First sign up to create initial credentials
237
+ const passphrase = await passphraseAuth.signUp();
238
+
239
+ // Log out
240
+ await contextManager.logOut();
241
+
242
+ // Log back in with passphrase
243
+ await passphraseAuth.logIn(passphrase);
244
+
245
+ // Verify we're logged in
246
+ const context = contextManager.getCurrentValue();
247
+
248
+ assert(context && "me" in context);
249
+
250
+ // Verify storage was updated
251
+ const storedData = await authSecretStorage.get();
252
+ expect(storedData?.provider).toBe("passphrase");
253
+ });
254
+
255
+ it("should throw error with invalid passphrase", async () => {
256
+ await expect(passphraseAuth.logIn("invalid words here")).rejects.toThrow(
257
+ "Invalid passphrase",
258
+ );
259
+ });
260
+ });
261
+
262
+ describe("signUp", () => {
263
+ it("should successfully sign up new user", async () => {
264
+ expect(authSecretStorage.isAuthenticated).toBe(false);
265
+
266
+ const passphrase = await passphraseAuth.signUp();
267
+
268
+ expect(authSecretStorage.isAuthenticated).toBe(true);
269
+
270
+ // Verify passphrase format
271
+ expect(passphrase.split(" ").length).toBeGreaterThan(0);
272
+
273
+ // Verify storage was updated
274
+ const storedData = await authSecretStorage.get();
275
+ expect(storedData?.provider).toBe("passphrase");
276
+
277
+ // Verify we can log in with the passphrase
278
+ await contextManager.logOut();
279
+ await passphraseAuth.logIn(passphrase);
280
+ const context = contextManager.getCurrentValue();
281
+ assert(context && "me" in context);
282
+ expect(context.me).toBeDefined();
283
+ });
284
+
285
+ it("should throw error when no credentials found", async () => {
286
+ await authSecretStorage.clear();
287
+ await expect(passphraseAuth.signUp()).rejects.toThrow(
288
+ "No credentials found",
289
+ );
290
+ });
291
+ });
292
+
293
+ describe("registerNewAccount", () => {
294
+ it("should successfully register new account with passphrase", async () => {
295
+ expect(authSecretStorage.isAuthenticated).toBe(false);
296
+
297
+ const passphrase = passphraseAuth.generateRandomPassphrase();
298
+ const accountId = await passphraseAuth.registerNewAccount(
299
+ passphrase,
300
+ "Test User",
301
+ );
302
+
303
+ // Verify account was created
304
+ expect(accountId).toBeDefined();
305
+
306
+ // Verify we can log in with the passphrase
307
+ await contextManager.logOut();
308
+ await passphraseAuth.logIn(passphrase);
309
+
310
+ const context = contextManager.getCurrentValue();
311
+
312
+ assert(context && "me" in context);
313
+ expect(context.me.id).toBe(accountId);
314
+ expect(context.me.profile?.name).toBe("Test User");
315
+
316
+ expect(authSecretStorage.isAuthenticated).toBe(true);
317
+
318
+ const credentials = await authSecretStorage.get();
319
+ assert(credentials);
320
+ expect(credentials.accountID).toBe(accountId);
321
+ expect(credentials.provider).toBe("passphrase");
322
+ });
323
+
324
+ it("should throw error with invalid passphrase during registration", async () => {
325
+ await expect(
326
+ passphraseAuth.registerNewAccount("invalid words", "Test User"),
327
+ ).rejects.toThrow();
328
+ });
329
+ });
330
+
331
+ describe("getCurrentAccountPassphrase", () => {
332
+ it("should return current user passphrase when credentials exist", async () => {
333
+ const originalPassphrase = await passphraseAuth.signUp();
334
+ const retrievedPassphrase =
335
+ await passphraseAuth.getCurrentAccountPassphrase();
336
+
337
+ expect(retrievedPassphrase).toBe(originalPassphrase);
338
+ });
339
+
340
+ it("should throw error when no credentials found", async () => {
341
+ await authSecretStorage.clear();
342
+ await expect(
343
+ passphraseAuth.getCurrentAccountPassphrase(),
344
+ ).rejects.toThrow("No credentials found");
345
+ });
346
+ });
347
+ });
package/src/types.ts CHANGED
@@ -12,6 +12,7 @@ export type AuthCredentials = {
12
12
  export type AuthenticateAccountFunction = (
13
13
  credentials: AuthCredentials,
14
14
  ) => Promise<void>;
15
+
15
16
  export type RegisterAccountFunction = (
16
17
  accountSecret: AgentSecret,
17
18
  creationProps: { name: string },
@@ -22,6 +23,7 @@ export type JazzAuthContext<Acc extends Account> = {
22
23
  me: Acc;
23
24
  node: LocalNode;
24
25
  authenticate: AuthenticateAccountFunction;
26
+ register: RegisterAccountFunction;
25
27
  logOut: () => Promise<void>;
26
28
  done: () => void;
27
29
  isAuthenticated?: boolean;
@@ -31,6 +33,7 @@ export type JazzGuestContext = {
31
33
  guest: AnonymousJazzAgent;
32
34
  node: LocalNode;
33
35
  authenticate: AuthenticateAccountFunction;
36
+ register: RegisterAccountFunction;
34
37
  logOut: () => void;
35
38
  done: () => void;
36
39
  isAuthenticated?: boolean;