jazz-tools 0.9.23 → 0.10.1

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 (56) hide show
  1. package/.turbo/turbo-build.log +10 -12
  2. package/CHANGELOG.md +27 -0
  3. package/dist/{chunk-OJIEP4WE.js → chunk-24EJ3CKA.js} +566 -118
  4. package/dist/chunk-24EJ3CKA.js.map +1 -0
  5. package/dist/{index.web.js → index.js} +20 -9
  6. package/dist/index.js.map +1 -0
  7. package/dist/testing.js +125 -34
  8. package/dist/testing.js.map +1 -1
  9. package/package.json +11 -15
  10. package/src/auth/AuthSecretStorage.ts +109 -0
  11. package/src/auth/DemoAuth.ts +188 -0
  12. package/src/auth/InMemoryKVStore.ts +25 -0
  13. package/src/auth/KvStoreContext.ts +39 -0
  14. package/src/auth/PassphraseAuth.ts +113 -0
  15. package/src/coValues/account.ts +8 -3
  16. package/src/coValues/coFeed.ts +1 -1
  17. package/src/coValues/coList.ts +1 -1
  18. package/src/coValues/coMap.ts +1 -1
  19. package/src/coValues/group.ts +9 -8
  20. package/src/coValues/interfaces.ts +14 -5
  21. package/src/exports.ts +17 -3
  22. package/src/implementation/ContextManager.ts +178 -0
  23. package/src/implementation/activeAccountContext.ts +6 -1
  24. package/src/implementation/createContext.ts +173 -149
  25. package/src/implementation/schema.ts +3 -3
  26. package/src/index.ts +3 -0
  27. package/src/testing.ts +172 -34
  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 +48 -42
  33. package/src/tests/coList.test.ts +26 -24
  34. package/src/tests/coMap.test.ts +25 -23
  35. package/src/tests/coPlainText.test.ts +25 -23
  36. package/src/tests/coRichText.test.ts +24 -23
  37. package/src/tests/createContext.test.ts +339 -0
  38. package/src/tests/deepLoading.test.ts +44 -45
  39. package/src/tests/fixtures.ts +2050 -0
  40. package/src/tests/groupsAndAccounts.test.ts +3 -3
  41. package/src/tests/schema.test.ts +1 -1
  42. package/src/tests/schemaUnion.test.ts +2 -2
  43. package/src/tests/subscribe.test.ts +43 -10
  44. package/src/tests/testing.test.ts +56 -0
  45. package/src/tests/utils.ts +13 -13
  46. package/src/types.ts +54 -0
  47. package/tsconfig.json +3 -1
  48. package/tsup.config.ts +1 -2
  49. package/dist/chunk-OJIEP4WE.js.map +0 -1
  50. package/dist/index.native.js +0 -75
  51. package/dist/index.native.js.map +0 -1
  52. package/dist/index.web.js.map +0 -1
  53. package/src/index.native.ts +0 -6
  54. package/src/index.web.ts +0 -3
  55. package/tsconfig.native.json +0 -5
  56. package/tsconfig.web.json +0 -5
package/src/testing.ts CHANGED
@@ -1,12 +1,22 @@
1
1
  import { AgentSecret, CryptoProvider, LocalNode, Peer } from "cojson";
2
2
  import { cojsonInternals } from "cojson";
3
- import { PureJSCrypto } from "cojson/crypto";
4
- import { Account, type AccountClass } from "./exports.js";
3
+ import { PureJSCrypto } from "cojson/dist/crypto/PureJSCrypto";
4
+ import {
5
+ Account,
6
+ AccountClass,
7
+ JazzContextManagerAuthProps,
8
+ } from "./exports.js";
9
+ import {
10
+ JazzContextManager,
11
+ JazzContextManagerBaseProps,
12
+ } from "./implementation/ContextManager.js";
5
13
  import { activeAccountContext } from "./implementation/activeAccountContext.js";
6
14
  import {
7
15
  type AnonymousJazzAgent,
8
16
  type CoValueClass,
9
17
  createAnonymousJazzContext,
18
+ createJazzContext,
19
+ randomSessionProvider,
10
20
  } from "./internal.js";
11
21
 
12
22
  const syncServer: { current: LocalNode | null } = { current: null };
@@ -21,9 +31,9 @@ type TestAccountSchema<Acc extends Account> = CoValueClass<Acc> & {
21
31
  }) => Promise<Acc>;
22
32
  };
23
33
 
24
- class TestJSCrypto extends PureJSCrypto {
34
+ export class TestJSCrypto extends PureJSCrypto {
25
35
  static async create() {
26
- if ("navigator" in globalThis && navigator.userAgent.includes("jsdom")) {
36
+ if ("navigator" in globalThis && navigator.userAgent?.includes("jsdom")) {
27
37
  // Mocking crypto seal & encrypt to make it work with JSDom. Getting "Error: Uint8Array expected" there
28
38
  const crypto = new PureJSCrypto();
29
39
 
@@ -44,6 +54,27 @@ class TestJSCrypto extends PureJSCrypto {
44
54
  }
45
55
  }
46
56
 
57
+ export function getPeerConnectedToTestSyncServer() {
58
+ if (!syncServer.current) {
59
+ throw new Error("Sync server not initialized");
60
+ }
61
+
62
+ const [aPeer, bPeer] = cojsonInternals.connectedPeers(
63
+ Math.random().toString(),
64
+ Math.random().toString(),
65
+ {
66
+ peer1role: "server",
67
+ peer2role: "server",
68
+ },
69
+ );
70
+ syncServer.current.syncManager.addPeer(aPeer);
71
+
72
+ return bPeer;
73
+ }
74
+
75
+ const SecretSeedMap = new Map<string, Uint8Array>();
76
+ let isMigrationActive = false;
77
+
47
78
  export async function createJazzTestAccount<Acc extends Account>(options?: {
48
79
  isCurrentActiveAccount?: boolean;
49
80
  AccountSchema?: CoValueClass<Acc>;
@@ -53,39 +84,50 @@ export async function createJazzTestAccount<Acc extends Account>(options?: {
53
84
  Account) as unknown as TestAccountSchema<Acc>;
54
85
  const peers = [];
55
86
  if (syncServer.current) {
56
- const [aPeer, bPeer] = cojsonInternals.connectedPeers(
57
- Math.random().toString(),
58
- Math.random().toString(),
59
- {
60
- peer1role: "server",
61
- peer2role: "server",
62
- },
63
- );
64
- syncServer.current.syncManager.addPeer(aPeer);
65
- peers.push(bPeer);
87
+ peers.push(getPeerConnectedToTestSyncServer());
66
88
  }
67
89
 
90
+ const crypto = await TestJSCrypto.create();
91
+ const secretSeed = crypto.newRandomSecretSeed();
92
+
68
93
  const { node } = await LocalNode.withNewlyCreatedAccount({
69
94
  creationProps: {
70
95
  name: "Test Account",
71
96
  ...options?.creationProps,
72
97
  },
73
- crypto: await TestJSCrypto.create(),
98
+ initialAgentSecret: crypto.agentSecretFromSecretSeed(secretSeed),
99
+ crypto,
74
100
  peersToLoadFrom: peers,
75
101
  migration: async (rawAccount, _node, creationProps) => {
102
+ if (isMigrationActive) {
103
+ throw new Error(
104
+ "It is not possible to create multiple accounts in parallel inside the test environment.",
105
+ );
106
+ }
107
+
108
+ isMigrationActive = true;
109
+
76
110
  const account = new AccountSchema({
77
111
  fromRaw: rawAccount,
78
112
  });
79
113
 
80
- if (options?.isCurrentActiveAccount) {
81
- activeAccountContext.set(account);
82
- }
114
+ // We need to set the account as current because the migration
115
+ // will probably rely on the global me
116
+ const prevActiveAccount = activeAccountContext.maybeGet();
117
+ activeAccountContext.set(account);
83
118
 
84
119
  await account.applyMigration?.(creationProps);
120
+
121
+ if (!options?.isCurrentActiveAccount) {
122
+ activeAccountContext.set(prevActiveAccount);
123
+ }
124
+
125
+ isMigrationActive = false;
85
126
  },
86
127
  });
87
128
 
88
129
  const account = AccountSchema.fromNode(node);
130
+ SecretSeedMap.set(account.id, secretSeed);
89
131
 
90
132
  if (options?.isCurrentActiveAccount) {
91
133
  activeAccountContext.set(account);
@@ -109,24 +151,120 @@ export async function createJazzTestGuest() {
109
151
  };
110
152
  }
111
153
 
112
- export function getJazzContextShape<Acc extends Account>(
113
- account: Acc | { guest: AnonymousJazzAgent },
114
- ) {
115
- if ("guest" in account) {
116
- return {
117
- guest: account.guest,
118
- AccountSchema: Account,
119
- logOut: () => account.guest.node.gracefulShutdown(),
120
- done: () => account.guest.node.gracefulShutdown(),
121
- };
154
+ export type TestJazzContextManagerProps<Acc extends Account> =
155
+ JazzContextManagerBaseProps<Acc> & {
156
+ defaultProfileName?: string;
157
+ AccountSchema?: AccountClass<Acc>;
158
+ isAuthenticated?: boolean;
159
+ };
160
+
161
+ export class TestJazzContextManager<
162
+ Acc extends Account,
163
+ > extends JazzContextManager<Acc, TestJazzContextManagerProps<Acc>> {
164
+ static fromAccountOrGuest<Acc extends Account>(
165
+ account?: Acc | { guest: AnonymousJazzAgent },
166
+ props?: TestJazzContextManagerProps<Acc>,
167
+ ) {
168
+ if (account && "guest" in account) {
169
+ return this.fromGuest<Acc>(account, props);
170
+ }
171
+
172
+ return this.fromAccount<Acc>(account ?? (Account.getMe() as Acc), props);
122
173
  }
123
174
 
124
- return {
125
- me: account,
126
- AccountSchema: account.constructor as AccountClass<Acc>,
127
- logOut: () => account._raw.core.node.gracefulShutdown(),
128
- done: () => account._raw.core.node.gracefulShutdown(),
129
- };
175
+ static fromAccount<Acc extends Account>(
176
+ account: Acc,
177
+ props?: TestJazzContextManagerProps<Acc>,
178
+ ) {
179
+ const context = new TestJazzContextManager<Acc>();
180
+
181
+ const provider = props?.isAuthenticated ? "testProvider" : "anonymous";
182
+ const storage = context.getAuthSecretStorage();
183
+ const node = account._raw.core.node;
184
+
185
+ storage.set({
186
+ accountID: account.id,
187
+ accountSecret: node.account.agentSecret,
188
+ secretSeed: SecretSeedMap.get(account.id),
189
+ provider,
190
+ });
191
+ storage.isAuthenticated = Boolean(props?.isAuthenticated);
192
+
193
+ context.updateContext(
194
+ {
195
+ AccountSchema: account.constructor as AccountClass<Acc>,
196
+ ...props,
197
+ },
198
+ {
199
+ me: account,
200
+ node,
201
+ done: () => {
202
+ node.gracefulShutdown();
203
+ },
204
+ logOut: async () => {
205
+ node.gracefulShutdown();
206
+ },
207
+ },
208
+ );
209
+
210
+ return context;
211
+ }
212
+
213
+ static fromGuest<Acc extends Account>(
214
+ { guest }: { guest: AnonymousJazzAgent },
215
+ props: TestJazzContextManagerProps<Acc> = {},
216
+ ) {
217
+ const context = new TestJazzContextManager<Acc>();
218
+ const node = guest.node;
219
+
220
+ context.updateContext(props, {
221
+ guest,
222
+ node,
223
+ done: () => {
224
+ node.gracefulShutdown();
225
+ },
226
+ logOut: async () => {
227
+ node.gracefulShutdown();
228
+ },
229
+ });
230
+
231
+ return context;
232
+ }
233
+
234
+ async createContext(
235
+ props: TestJazzContextManagerProps<Acc>,
236
+ authProps?: JazzContextManagerAuthProps,
237
+ ) {
238
+ this.props = props;
239
+
240
+ if (!syncServer.current) {
241
+ throw new Error(
242
+ "You need to setup a test sync server with setupJazzTestSync to use the Auth functions",
243
+ );
244
+ }
245
+
246
+ const context = await createJazzContext<Acc>({
247
+ credentials: authProps?.credentials,
248
+ defaultProfileName: props.defaultProfileName,
249
+ newAccountProps: authProps?.newAccountProps,
250
+ peersToLoadFrom: [getPeerConnectedToTestSyncServer()],
251
+ crypto: await TestJSCrypto.create(),
252
+ sessionProvider: randomSessionProvider,
253
+ authSecretStorage: this.getAuthSecretStorage(),
254
+ AccountSchema: props.AccountSchema,
255
+ });
256
+
257
+ this.updateContext(props, {
258
+ me: context.account,
259
+ node: context.node,
260
+ done: () => {
261
+ context.done();
262
+ },
263
+ logOut: () => {
264
+ return context.logOut();
265
+ },
266
+ });
267
+ }
130
268
  }
131
269
 
132
270
  export function linkAccounts(
@@ -0,0 +1,275 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { Account } from "jazz-tools";
4
+ import { ID } from "jazz-tools";
5
+ import { beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { AuthSecretStorage } from "../auth/AuthSecretStorage";
7
+ import { InMemoryKVStore } from "../auth/InMemoryKVStore.js";
8
+ import KvStoreContext from "../auth/KvStoreContext";
9
+
10
+ const kvStore = new InMemoryKVStore();
11
+ KvStoreContext.getInstance().initialize(kvStore);
12
+
13
+ let authSecretStorage = new AuthSecretStorage();
14
+
15
+ describe("AuthSecretStorage", () => {
16
+ beforeEach(() => {
17
+ kvStore.clearAll();
18
+ authSecretStorage = new AuthSecretStorage();
19
+ });
20
+
21
+ describe("migrate", () => {
22
+ it("should migrate demo auth secret", async () => {
23
+ const demoSecret = JSON.stringify({
24
+ accountID: "demo123",
25
+ accountSecret: "secret123",
26
+ });
27
+ await kvStore.set("demo-auth-logged-in-secret", demoSecret);
28
+
29
+ await authSecretStorage.migrate();
30
+
31
+ expect(await kvStore.get("jazz-logged-in-secret")).toBe(demoSecret);
32
+ expect(await kvStore.get("demo-auth-logged-in-secret")).toBeNull();
33
+ });
34
+
35
+ it("should migrate clerk auth secret", async () => {
36
+ const clerkSecret = JSON.stringify({
37
+ accountID: "clerk123",
38
+ accountSecret: "secret123",
39
+ });
40
+ await kvStore.set("jazz-clerk-auth", clerkSecret);
41
+
42
+ await authSecretStorage.migrate();
43
+
44
+ expect(await kvStore.get("jazz-logged-in-secret")).toBe(clerkSecret);
45
+ expect(await kvStore.get("jazz-clerk-auth")).toBeNull();
46
+ });
47
+ });
48
+
49
+ describe("get", () => {
50
+ it("should return null when no data exists", async () => {
51
+ expect(await authSecretStorage.get()).toBeNull();
52
+ });
53
+
54
+ it("should return credentials with secretSeed", async () => {
55
+ const credentials = {
56
+ accountID: "test123",
57
+ secretSeed: [1, 2, 3],
58
+ accountSecret: "secret123",
59
+ provider: "anonymous",
60
+ };
61
+ await kvStore.set("jazz-logged-in-secret", JSON.stringify(credentials));
62
+
63
+ const result = await authSecretStorage.get();
64
+
65
+ expect(result).toEqual({
66
+ accountID: "test123",
67
+ secretSeed: new Uint8Array([1, 2, 3]),
68
+ accountSecret: "secret123",
69
+ provider: "anonymous",
70
+ });
71
+ });
72
+
73
+ it("should return non-anonymous credentials without secretSeed", async () => {
74
+ const credentials = {
75
+ accountID: "test123",
76
+ accountSecret: "secret123",
77
+ provider: "passphrase",
78
+ };
79
+ await kvStore.set("jazz-logged-in-secret", JSON.stringify(credentials));
80
+
81
+ const result = await authSecretStorage.get();
82
+
83
+ expect(result).toEqual({
84
+ accountID: "test123",
85
+ accountSecret: "secret123",
86
+ provider: "passphrase",
87
+ });
88
+ });
89
+
90
+ it("should throw error for invalid data", async () => {
91
+ await kvStore.set(
92
+ "jazz-logged-in-secret",
93
+ JSON.stringify({ invalid: "data" }),
94
+ );
95
+
96
+ await expect(authSecretStorage.get()).rejects.toThrow(
97
+ "Invalid auth secret storage data",
98
+ );
99
+ });
100
+ });
101
+
102
+ describe("set", () => {
103
+ it("should set credentials with secretSeed", async () => {
104
+ const payload = {
105
+ accountID: "test123" as ID<Account>,
106
+ secretSeed: new Uint8Array([1, 2, 3]),
107
+ accountSecret:
108
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
109
+ provider: "passphrase",
110
+ };
111
+
112
+ await authSecretStorage.set(payload);
113
+
114
+ const stored = JSON.parse((await kvStore.get("jazz-logged-in-secret"))!);
115
+ expect(stored).toEqual({
116
+ accountID: "test123",
117
+ secretSeed: [1, 2, 3],
118
+ accountSecret: "secret123",
119
+ provider: "passphrase",
120
+ });
121
+ });
122
+
123
+ it("should set credentials without secretSeed", async () => {
124
+ const payload = {
125
+ accountID: "test123" as ID<Account>,
126
+ accountSecret:
127
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
128
+ provider: "passphrase",
129
+ };
130
+
131
+ await authSecretStorage.set(payload);
132
+
133
+ const stored = JSON.parse((await kvStore.get("jazz-logged-in-secret"))!);
134
+ expect(stored).toEqual(payload);
135
+ });
136
+
137
+ it("should emit update event when setting credentials", async () => {
138
+ const handler = vi.fn();
139
+ authSecretStorage.onUpdate(handler);
140
+
141
+ await authSecretStorage.set({
142
+ accountID: "test123" as ID<Account>,
143
+ accountSecret:
144
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
145
+ provider: "passphrase",
146
+ });
147
+
148
+ expect(handler).toHaveBeenCalled();
149
+ });
150
+ });
151
+
152
+ describe("isAuthenticated", () => {
153
+ it("should return false when no data exists", async () => {
154
+ expect(authSecretStorage.isAuthenticated).toBe(false);
155
+ });
156
+
157
+ it("should return false for anonymous credentials", async () => {
158
+ await authSecretStorage.set({
159
+ accountID: "test123" as ID<Account>,
160
+ accountSecret:
161
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
162
+ secretSeed: new Uint8Array([1, 2, 3]),
163
+ provider: "anonymous",
164
+ });
165
+ expect(authSecretStorage.isAuthenticated).toBe(false);
166
+ });
167
+
168
+ it("should return true for non-anonymous credentials", async () => {
169
+ await authSecretStorage.set({
170
+ accountID: "test123" as ID<Account>,
171
+ accountSecret:
172
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
173
+ secretSeed: new Uint8Array([1, 2, 3]),
174
+ provider: "demo",
175
+ });
176
+ expect(authSecretStorage.isAuthenticated).toBe(true);
177
+ });
178
+
179
+ it("should return true when the provider is missing", async () => {
180
+ await authSecretStorage.set({
181
+ accountID: "test123" as ID<Account>,
182
+ accountSecret:
183
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
184
+ secretSeed: new Uint8Array([1, 2, 3]),
185
+ } as any);
186
+ expect(authSecretStorage.isAuthenticated).toBe(true);
187
+ });
188
+ });
189
+
190
+ describe("onUpdate", () => {
191
+ it("should add and remove event listener", () => {
192
+ const handler = vi.fn();
193
+
194
+ const removeListener = authSecretStorage.onUpdate(handler);
195
+
196
+ authSecretStorage.emitUpdate({
197
+ accountID: "test123" as ID<Account>,
198
+ accountSecret:
199
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
200
+ secretSeed: new Uint8Array([1, 2, 3]),
201
+ provider: "demo",
202
+ });
203
+ expect(handler).toHaveBeenCalledTimes(1);
204
+
205
+ handler.mockClear();
206
+
207
+ removeListener();
208
+ authSecretStorage.emitUpdate({
209
+ accountID: "test123" as ID<Account>,
210
+ accountSecret:
211
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
212
+ secretSeed: new Uint8Array([1, 2, 3]),
213
+ provider: "anonymous",
214
+ });
215
+ expect(handler).not.toHaveBeenCalled();
216
+ });
217
+
218
+ it("should emit events only when the isAuthenticated state changes", () => {
219
+ const handler = vi.fn();
220
+
221
+ authSecretStorage.onUpdate(handler);
222
+
223
+ authSecretStorage.emitUpdate({
224
+ accountID: "test123" as ID<Account>,
225
+ accountSecret:
226
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
227
+ secretSeed: new Uint8Array([1, 2, 3]),
228
+ provider: "demo",
229
+ });
230
+ expect(handler).toHaveBeenCalledTimes(1);
231
+
232
+ handler.mockClear();
233
+
234
+ authSecretStorage.emitUpdate({
235
+ accountID: "test123" as ID<Account>,
236
+ accountSecret:
237
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
238
+ secretSeed: new Uint8Array([1, 2, 3]),
239
+ provider: "demo",
240
+ });
241
+ expect(handler).not.toHaveBeenCalled();
242
+ });
243
+ });
244
+
245
+ describe("clear", () => {
246
+ it("should remove stored credentials", async () => {
247
+ await authSecretStorage.set({
248
+ accountID: "test123" as ID<Account>,
249
+ accountSecret:
250
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
251
+ provider: "passphrase",
252
+ });
253
+
254
+ await authSecretStorage.clear();
255
+
256
+ expect(await authSecretStorage.get()).toBeNull();
257
+ });
258
+
259
+ it("should emit update event when clearing", async () => {
260
+ await authSecretStorage.set({
261
+ accountID: "test123" as ID<Account>,
262
+ accountSecret:
263
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
264
+ provider: "passphrase",
265
+ });
266
+
267
+ const handler = vi.fn();
268
+ authSecretStorage.onUpdate(handler);
269
+
270
+ await authSecretStorage.clear();
271
+
272
+ expect(handler).toHaveBeenCalled();
273
+ });
274
+ });
275
+ });