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,188 @@
1
+ import { AgentSecret } from "cojson";
2
+ import { Account } from "../coValues/account.js";
3
+ import { ID } from "../internal.js";
4
+ import { AuthenticateAccountFunction } from "../types.js";
5
+ import { AuthSecretStorage } from "./AuthSecretStorage.js";
6
+ import { KvStore, KvStoreContext } from "./KvStoreContext.js";
7
+
8
+ type StorageData = {
9
+ accountID: ID<Account>;
10
+ accountSecret: AgentSecret;
11
+ secretSeed?: number[];
12
+ };
13
+
14
+ /**
15
+ * `DemoAuth` provides a `JazzAuth` object for demo authentication.
16
+ *
17
+ * Demo authentication is useful for quickly testing your app, as it allows you to create new accounts and log in as existing ones.
18
+ *
19
+ * ```
20
+ * import { DemoAuth } from "jazz-tools";
21
+ *
22
+ * const auth = new DemoAuth(jazzContext.authenticate, new AuthSecretStorage());
23
+ * ```
24
+ *
25
+ * @category Auth Providers
26
+ */
27
+ export class DemoAuth {
28
+ constructor(
29
+ private authenticate: AuthenticateAccountFunction,
30
+ private authSecretStorage: AuthSecretStorage,
31
+ ) {}
32
+
33
+ logIn = async (username: string) => {
34
+ const existingUsers = await this.getExisitingUsersWithData();
35
+ const storageData = existingUsers[username];
36
+
37
+ if (!storageData?.accountID) {
38
+ throw new Error("User not found");
39
+ }
40
+
41
+ await this.authenticate({
42
+ accountID: storageData.accountID,
43
+ accountSecret: storageData.accountSecret,
44
+ });
45
+
46
+ await this.authSecretStorage.set({
47
+ accountID: storageData.accountID,
48
+ accountSecret: storageData.accountSecret,
49
+ secretSeed: storageData.secretSeed
50
+ ? new Uint8Array(storageData.secretSeed)
51
+ : undefined,
52
+ provider: "demo",
53
+ });
54
+ };
55
+
56
+ signUp = async (username: string) => {
57
+ const existingUsers = await this.getExistingUsers();
58
+ if (existingUsers.includes(username)) {
59
+ throw new Error("User already registered");
60
+ }
61
+
62
+ const credentials = await this.authSecretStorage.get();
63
+
64
+ if (!credentials) {
65
+ throw new Error("No credentials found");
66
+ }
67
+
68
+ const currentAccount = await Account.getMe().ensureLoaded({
69
+ profile: {},
70
+ });
71
+
72
+ currentAccount.profile.name = username;
73
+
74
+ await this.authSecretStorage.set({
75
+ accountID: credentials.accountID,
76
+ accountSecret: credentials.accountSecret,
77
+ secretSeed: credentials.secretSeed
78
+ ? new Uint8Array(credentials.secretSeed)
79
+ : undefined,
80
+ provider: "demo",
81
+ });
82
+
83
+ await this.addToExistingUsers(username, {
84
+ accountID: credentials.accountID,
85
+ accountSecret: credentials.accountSecret,
86
+ secretSeed: credentials.secretSeed
87
+ ? Array.from(credentials.secretSeed)
88
+ : undefined,
89
+ });
90
+ };
91
+
92
+ private async addToExistingUsers(username: string, data: StorageData) {
93
+ const existingUsers = await this.getExisitingUsersWithData();
94
+
95
+ if (existingUsers[username]) {
96
+ return;
97
+ }
98
+
99
+ existingUsers[username] = data;
100
+
101
+ const kvStore = KvStoreContext.getInstance().getStorage();
102
+ await kvStore.set("demo-auth-users", JSON.stringify(existingUsers));
103
+ }
104
+
105
+ private async getExisitingUsersWithData() {
106
+ const kvStore = KvStoreContext.getInstance().getStorage();
107
+ await migrateExistingUsers(kvStore);
108
+
109
+ const existingUsers = await kvStore.get("demo-auth-users");
110
+ return existingUsers ? JSON.parse(existingUsers) : {};
111
+ }
112
+
113
+ getExistingUsers = async () => {
114
+ return Object.keys(await this.getExisitingUsersWithData());
115
+ };
116
+ }
117
+
118
+ export function encodeUsername(username: string) {
119
+ return btoa(username)
120
+ .replace(/=/g, "-")
121
+ .replace(/\+/g, "_")
122
+ .replace(/\//g, ".");
123
+ }
124
+
125
+ async function getStorageVersion(kvStore: KvStore) {
126
+ try {
127
+ const version = await kvStore.get("demo-auth-storage-version");
128
+ return version ? parseInt(version) : 1;
129
+ } catch (error) {
130
+ return 1;
131
+ }
132
+ }
133
+
134
+ async function setStorageVersion(kvStore: KvStore, version: number) {
135
+ await kvStore.set("demo-auth-storage-version", version.toString());
136
+ }
137
+
138
+ async function getExistingUsersList(kvStore: KvStore) {
139
+ const existingUsers = await kvStore.get("demo-auth-existing-users");
140
+ return existingUsers ? existingUsers.split(",") : [];
141
+ }
142
+
143
+ /**
144
+ * Migrates existing users keys to work with any storage.
145
+ */
146
+ async function migrateExistingUsers(kvStore: KvStore) {
147
+ if ((await getStorageVersion(kvStore)) < 2) {
148
+ const existingUsers = await getExistingUsersList(kvStore);
149
+
150
+ for (const username of existingUsers) {
151
+ const legacyKey = `demo-auth-existing-users-${username}`;
152
+ const storageData = await kvStore.get(legacyKey);
153
+ if (storageData) {
154
+ await kvStore.set(
155
+ `demo-auth-existing-users-${encodeUsername(username)}`,
156
+ storageData,
157
+ );
158
+ await kvStore.delete(legacyKey);
159
+ }
160
+ }
161
+
162
+ await setStorageVersion(kvStore, 2);
163
+ }
164
+
165
+ if ((await getStorageVersion(kvStore)) < 3) {
166
+ const existingUsersList = await getExistingUsersList(kvStore);
167
+
168
+ const existingUsers: Record<string, StorageData> = {};
169
+ const keysToDelete: string[] = ["demo-auth-existing-users"];
170
+
171
+ for (const username of existingUsersList) {
172
+ const key = `demo-auth-existing-users-${encodeUsername(username)}`;
173
+ const storageData = await kvStore.get(key);
174
+ if (storageData) {
175
+ existingUsers[username] = JSON.parse(storageData);
176
+ keysToDelete.push(key);
177
+ }
178
+ }
179
+
180
+ await kvStore.set("demo-auth-users", JSON.stringify(existingUsers));
181
+
182
+ for (const key of keysToDelete) {
183
+ await kvStore.delete(key);
184
+ }
185
+
186
+ await setStorageVersion(kvStore, 3);
187
+ }
188
+ }
@@ -0,0 +1,25 @@
1
+ import { KvStore } from "./KvStoreContext.js";
2
+
3
+ export class InMemoryKVStore implements KvStore {
4
+ private store: Record<string, string> = {};
5
+
6
+ async get(key: string) {
7
+ const data = this.store[key];
8
+
9
+ if (!data) return null;
10
+
11
+ return data;
12
+ }
13
+
14
+ async set(key: string, value: string) {
15
+ this.store[key] = value;
16
+ }
17
+
18
+ async delete(key: string) {
19
+ delete this.store[key];
20
+ }
21
+
22
+ async clearAll() {
23
+ this.store = {};
24
+ }
25
+ }
@@ -0,0 +1,39 @@
1
+ export interface KvStore {
2
+ get(key: string): Promise<string | null>;
3
+ set(key: string, value: string): Promise<void>;
4
+ delete(key: string): Promise<void>;
5
+ clearAll(): Promise<void>;
6
+ }
7
+
8
+ export class KvStoreContext {
9
+ private static instance: KvStoreContext;
10
+ private storageInstance: KvStore | null = null;
11
+
12
+ private constructor() {}
13
+
14
+ public static getInstance(): KvStoreContext {
15
+ if (!KvStoreContext.instance) {
16
+ KvStoreContext.instance = new KvStoreContext();
17
+ }
18
+ return KvStoreContext.instance;
19
+ }
20
+
21
+ public isInitialized(): boolean {
22
+ return this.storageInstance !== null;
23
+ }
24
+
25
+ public initialize(store: KvStore): void {
26
+ if (!this.storageInstance) {
27
+ this.storageInstance = store;
28
+ }
29
+ }
30
+
31
+ public getStorage(): KvStore {
32
+ if (!this.storageInstance) {
33
+ throw new Error("Storage instance is not initialized.");
34
+ }
35
+ return this.storageInstance;
36
+ }
37
+ }
38
+
39
+ export default KvStoreContext;
@@ -0,0 +1,113 @@
1
+ import * as bip39 from "@scure/bip39";
2
+ import { entropyToMnemonic } from "@scure/bip39";
3
+ import { CryptoProvider, cojsonInternals } from "cojson";
4
+ import { Account } from "../coValues/account.js";
5
+ import type { ID } from "../internal.js";
6
+ import type { AuthenticateAccountFunction } from "../types.js";
7
+ import { AuthSecretStorage } from "./AuthSecretStorage.js";
8
+
9
+ /**
10
+ * `PassphraseAuth` provides a `JazzAuth` object for passphrase authentication.
11
+ *
12
+ * ```ts
13
+ * import { PassphraseAuth } from "jazz-tools";
14
+ *
15
+ * const auth = new PassphraseAuth(crypto, jazzContext.authenticate, new AuthSecretStorage(), wordlist);
16
+ * ```
17
+ *
18
+ * @category Auth Providers
19
+ */
20
+ export class PassphraseAuth {
21
+ passphrase: string = "";
22
+
23
+ constructor(
24
+ private crypto: CryptoProvider,
25
+ private authenticate: AuthenticateAccountFunction,
26
+ private authSecretStorage: AuthSecretStorage,
27
+ public wordlist: string[],
28
+ ) {}
29
+
30
+ logIn = async (passphrase: string) => {
31
+ const { crypto, authenticate } = this;
32
+
33
+ let secretSeed;
34
+
35
+ try {
36
+ secretSeed = bip39.mnemonicToEntropy(passphrase, this.wordlist);
37
+ } catch (e) {
38
+ throw new Error("Invalid passphrase");
39
+ }
40
+
41
+ const accountSecret = crypto.agentSecretFromSecretSeed(secretSeed);
42
+
43
+ const accountID = cojsonInternals.idforHeader(
44
+ cojsonInternals.accountHeaderForInitialAgentSecret(accountSecret, crypto),
45
+ crypto,
46
+ ) as ID<Account>;
47
+
48
+ await authenticate({
49
+ accountID,
50
+ accountSecret,
51
+ });
52
+
53
+ await this.authSecretStorage.set({
54
+ accountID,
55
+ secretSeed,
56
+ accountSecret,
57
+ provider: "passphrase",
58
+ });
59
+
60
+ this.passphrase = passphrase;
61
+ this.notify();
62
+ };
63
+
64
+ signUp = async () => {
65
+ const credentials = await this.authSecretStorage.get();
66
+
67
+ if (!credentials || !credentials.secretSeed) {
68
+ throw new Error("No credentials found");
69
+ }
70
+
71
+ const passphrase = entropyToMnemonic(credentials.secretSeed, this.wordlist);
72
+
73
+ await this.authSecretStorage.set({
74
+ accountID: credentials.accountID,
75
+ secretSeed: credentials.secretSeed,
76
+ accountSecret: credentials.accountSecret,
77
+ provider: "passphrase",
78
+ });
79
+
80
+ return passphrase;
81
+ };
82
+
83
+ getCurrentAccountPassphrase = async () => {
84
+ const credentials = await this.authSecretStorage.get();
85
+
86
+ if (!credentials || !credentials.secretSeed) {
87
+ throw new Error("No credentials found");
88
+ }
89
+
90
+ return entropyToMnemonic(credentials.secretSeed, this.wordlist);
91
+ };
92
+
93
+ loadCurrentAccountPassphrase = async () => {
94
+ const passphrase = await this.getCurrentAccountPassphrase();
95
+ this.passphrase = passphrase;
96
+ this.notify();
97
+ };
98
+
99
+ listeners = new Set<() => void>();
100
+ subscribe = (callback: () => void) => {
101
+ this.listeners.add(callback);
102
+
103
+ return () => {
104
+ this.listeners.delete(callback);
105
+ };
106
+ };
107
+
108
+ notify() {
109
+ for (const listener of this.listeners) {
110
+ listener();
111
+ }
112
+ }
113
+ }
@@ -42,6 +42,11 @@ import { createInboxRoot } from "./inbox.js";
42
42
  import { Profile } from "./profile.js";
43
43
  import { RegisteredSchemas } from "./registeredSchemas.js";
44
44
 
45
+ export type AccountCreationProps = {
46
+ name: string;
47
+ onboarding?: boolean;
48
+ };
49
+
45
50
  /** @category Identity & Permissions */
46
51
  export class Account extends CoValueBase implements CoValue {
47
52
  declare id: ID<this>;
@@ -253,7 +258,7 @@ export class Account extends CoValueBase implements CoValue {
253
258
  return this.toJSON();
254
259
  }
255
260
 
256
- async applyMigration(creationProps?: { name: string; onboarding?: boolean }) {
261
+ async applyMigration(creationProps?: AccountCreationProps) {
257
262
  if (creationProps) {
258
263
  const profileGroup = RegisteredSchemas["Group"].create({ owner: this });
259
264
  profileGroup.addMember("everyone", "reader");
@@ -278,7 +283,7 @@ export class Account extends CoValueBase implements CoValue {
278
283
  }
279
284
 
280
285
  // Placeholder method for subclasses to override
281
- migrate(creationProps?: { name: string }) {
286
+ migrate(creationProps?: AccountCreationProps) {
282
287
  creationProps; // To avoid unused parameter warning
283
288
  }
284
289
 
@@ -339,7 +344,7 @@ export class Account extends CoValueBase implements CoValue {
339
344
  ensureLoaded<A extends Account, Depth>(
340
345
  this: A,
341
346
  depth: Depth & DepthsIn<A>,
342
- ): Promise<DeeplyLoaded<A, Depth> | undefined> {
347
+ ): Promise<DeeplyLoaded<A, Depth>> {
343
348
  return ensureCoValueLoaded(this, depth);
344
349
  }
345
350
 
@@ -391,7 +391,7 @@ export class CoFeed<Item = any> extends CoValueBase implements CoValue {
391
391
  ensureLoaded<S extends CoFeed, Depth>(
392
392
  this: S,
393
393
  depth: Depth & DepthsIn<S>,
394
- ): Promise<DeeplyLoaded<S, Depth> | undefined> {
394
+ ): Promise<DeeplyLoaded<S, Depth>> {
395
395
  return ensureCoValueLoaded(this, depth);
396
396
  }
397
397
 
@@ -449,7 +449,7 @@ export class CoList<Item = any> extends Array<Item> implements CoValue {
449
449
  ensureLoaded<L extends CoList, Depth>(
450
450
  this: L,
451
451
  depth: Depth & DepthsIn<L>,
452
- ): Promise<DeeplyLoaded<L, Depth> | undefined> {
452
+ ): Promise<DeeplyLoaded<L, Depth>> {
453
453
  return ensureCoValueLoaded(this, depth);
454
454
  }
455
455
 
@@ -542,7 +542,7 @@ export class CoMap extends CoValueBase implements CoValue {
542
542
  ensureLoaded<M extends CoMap, Depth>(
543
543
  this: M,
544
544
  depth: Depth & DepthsIn<M>,
545
- ): Promise<DeeplyLoaded<M, Depth> | undefined> {
545
+ ): Promise<DeeplyLoaded<M, Depth>> {
546
546
  return ensureCoValueLoaded(this, depth);
547
547
  }
548
548
 
@@ -139,16 +139,14 @@ export class Group extends CoValueBase implements CoValue {
139
139
  return this._raw.myRole();
140
140
  }
141
141
 
142
- addMember(member: Everyone, role: "writer" | "reader"): Group;
143
- addMember(member: Account, role: AccountRole): Group;
142
+ addMember(member: Everyone, role: "writer" | "reader"): void;
143
+ addMember(member: Account, role: AccountRole): void;
144
144
  addMember(member: Everyone | Account, role: AccountRole) {
145
145
  this._raw.addMember(member === "everyone" ? member : member._raw, role);
146
- return this;
147
146
  }
148
147
 
149
148
  removeMember(member: Everyone | Account) {
150
- this._raw.removeMember(member === "everyone" ? member : member._raw);
151
- return this;
149
+ return this._raw.removeMember(member === "everyone" ? member : member._raw);
152
150
  }
153
151
 
154
152
  get members() {
@@ -181,8 +179,11 @@ export class Group extends CoValueBase implements CoValue {
181
179
  });
182
180
  }
183
181
 
184
- extend(parent: Group) {
185
- this._raw.extend(parent._raw);
182
+ extend(
183
+ parent: Group,
184
+ roleMapping?: "reader" | "writer" | "admin" | "inherit",
185
+ ) {
186
+ this._raw.extend(parent._raw, roleMapping);
186
187
  return this;
187
188
  }
188
189
 
@@ -243,7 +244,7 @@ export class Group extends CoValueBase implements CoValue {
243
244
  ensureLoaded<G extends Group, Depth>(
244
245
  this: G,
245
246
  depth: Depth & DepthsIn<G>,
246
- ): Promise<DeeplyLoaded<G, Depth> | undefined> {
247
+ ): Promise<DeeplyLoaded<G, Depth>> {
247
248
  return ensureCoValueLoaded(this, depth);
248
249
  }
249
250
 
@@ -196,16 +196,22 @@ export function loadCoValue<V extends CoValue, Depth>(
196
196
  });
197
197
  }
198
198
 
199
- export function ensureCoValueLoaded<V extends CoValue, Depth>(
199
+ export async function ensureCoValueLoaded<V extends CoValue, Depth>(
200
200
  existing: V,
201
201
  depth: Depth & DepthsIn<V>,
202
- ): Promise<DeeplyLoaded<V, Depth> | undefined> {
203
- return loadCoValue(
202
+ ): Promise<DeeplyLoaded<V, Depth>> {
203
+ const response = await loadCoValue(
204
204
  existing.constructor as CoValueClass<V>,
205
205
  existing.id,
206
206
  existing._loadedAs,
207
207
  depth,
208
208
  );
209
+
210
+ if (!response) {
211
+ throw new Error("Failed to deeply load CoValue " + existing.id);
212
+ }
213
+
214
+ return response;
209
215
  }
210
216
 
211
217
  export function subscribeToCoValueWithoutMe<V extends CoValue, Depth>(
@@ -301,7 +307,7 @@ export function subscribeToCoValue<V extends CoValue, Depth>(
301
307
  export function createCoValueObservable<V extends CoValue, Depth>(options?: {
302
308
  syncResolution?: boolean;
303
309
  }) {
304
- let currentValue: DeeplyLoaded<V, Depth> | undefined = undefined;
310
+ let currentValue: DeeplyLoaded<V, Depth> | undefined | null = undefined;
305
311
  let subscriberCount = 0;
306
312
 
307
313
  function subscribe(
@@ -323,7 +329,10 @@ export function createCoValueObservable<V extends CoValue, Depth>(options?: {
323
329
  currentValue = value;
324
330
  listener();
325
331
  },
326
- onUnavailable,
332
+ () => {
333
+ currentValue = null;
334
+ onUnavailable?.();
335
+ },
327
336
  options?.syncResolution,
328
337
  );
329
338
 
package/src/exports.ts CHANGED
@@ -18,6 +18,7 @@ export {
18
18
  Account,
19
19
  isControlledAccount,
20
20
  type AccountClass,
21
+ type AccountCreationProps,
21
22
  } from "./coValues/account.js";
22
23
  export {
23
24
  BinaryCoStream,
@@ -49,6 +50,17 @@ export {
49
50
  subscribeToCoValue,
50
51
  } from "./internal.js";
51
52
 
53
+ export {
54
+ JazzContextManager,
55
+ type JazzContextManagerAuthProps,
56
+ } from "./implementation/ContextManager.js";
57
+
58
+ export { AuthSecretStorage } from "./auth/AuthSecretStorage.js";
59
+ export { KvStoreContext, type KvStore } from "./auth/KvStoreContext.js";
60
+ export { InMemoryKVStore } from "./auth/InMemoryKVStore.js";
61
+ export { DemoAuth } from "./auth/DemoAuth.js";
62
+ export { PassphraseAuth } from "./auth/PassphraseAuth.js";
63
+
52
64
  export {
53
65
  createInviteLink,
54
66
  parseInviteLink,
@@ -58,11 +70,13 @@ export {
58
70
  export {
59
71
  AnonymousJazzAgent,
60
72
  createAnonymousJazzContext,
73
+ createJazzContextFromExistingCredentials,
74
+ createJazzContextForNewAccount,
61
75
  createJazzContext,
62
- ephemeralCredentialsAuth,
63
- fixedCredentialsAuth,
64
76
  randomSessionProvider,
65
- type AuthMethod,
66
77
  type AuthResult,
67
78
  type Credentials,
79
+ type JazzContextWithAccount,
68
80
  } from "./internal.js";
81
+
82
+ export type * from "./types.js";