jazz-tools 0.19.16 → 0.19.17

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 (39) hide show
  1. package/.svelte-kit/__package__/tests/media/image.svelte.test.js +4 -1
  2. package/.turbo/turbo-build.log +55 -55
  3. package/CHANGELOG.md +9 -0
  4. package/dist/{chunk-R3KIZG4P.js → chunk-OH2GW5WP.js} +24 -8
  5. package/dist/{chunk-R3KIZG4P.js.map → chunk-OH2GW5WP.js.map} +1 -1
  6. package/dist/index.js +72 -48
  7. package/dist/index.js.map +1 -1
  8. package/dist/react-native/index.js +18 -17
  9. package/dist/react-native/index.js.map +1 -1
  10. package/dist/react-native-core/ReactNativeSessionProvider.d.ts.map +1 -1
  11. package/dist/react-native-core/index.js +18 -17
  12. package/dist/react-native-core/index.js.map +1 -1
  13. package/dist/svelte/tests/media/image.svelte.test.js +4 -1
  14. package/dist/testing.js +1 -1
  15. package/dist/tools/auth/clerk/getClerkUsername.d.ts +2 -2
  16. package/dist/tools/auth/clerk/getClerkUsername.d.ts.map +1 -1
  17. package/dist/tools/auth/clerk/index.d.ts +4 -4
  18. package/dist/tools/auth/clerk/index.d.ts.map +1 -1
  19. package/dist/tools/auth/clerk/tests/isClerkCredentials.test.d.ts +2 -0
  20. package/dist/tools/auth/clerk/tests/isClerkCredentials.test.d.ts.map +1 -0
  21. package/dist/tools/auth/clerk/types.d.ts +62 -19
  22. package/dist/tools/auth/clerk/types.d.ts.map +1 -1
  23. package/dist/tools/implementation/ContextManager.d.ts +10 -0
  24. package/dist/tools/implementation/ContextManager.d.ts.map +1 -1
  25. package/package.json +5 -6
  26. package/src/react/tests/media/image.test.tsx +5 -1
  27. package/src/react-native-core/ReactNativeSessionProvider.ts +24 -24
  28. package/src/svelte/tests/media/image.svelte.test.ts +5 -1
  29. package/src/tools/auth/clerk/getClerkUsername.ts +13 -20
  30. package/src/tools/auth/clerk/index.ts +25 -28
  31. package/src/tools/auth/clerk/tests/JazzClerkAuth.test.ts +29 -34
  32. package/src/tools/auth/clerk/tests/getClerkUsername.test.ts +25 -45
  33. package/src/tools/auth/clerk/tests/isClerkAuthStateEqual.test.ts +7 -3
  34. package/src/tools/auth/clerk/tests/{types.test.ts → isClerkCredentials.test.ts} +4 -2
  35. package/src/tools/auth/clerk/types.ts +55 -34
  36. package/src/tools/implementation/ContextManager.ts +28 -7
  37. package/src/tools/tests/ContextManager.test.ts +16 -0
  38. package/dist/tools/auth/clerk/tests/types.test.d.ts +0 -2
  39. package/dist/tools/auth/clerk/tests/types.test.d.ts.map +0 -1
@@ -1,81 +1,61 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { getClerkUsername } from "../getClerkUsername.js";
3
- import type { MinimalClerkClient } from "../types.js";
3
+ import type { ClerkUser } from "../types.js";
4
4
 
5
5
  describe("getClerkUsername", () => {
6
- it("should return null if no user", () => {
7
- const mockClerk = {
8
- user: null,
9
- } as MinimalClerkClient;
10
-
11
- expect(getClerkUsername(mockClerk)).toBe(null);
12
- });
13
-
14
6
  it("should return fullName if available", () => {
15
- const mockClerk = {
16
- user: {
7
+ expect(
8
+ getClerkUsername({
17
9
  fullName: "John Doe",
18
10
  firstName: "John",
19
11
  lastName: "Doe",
20
12
  username: "johndoe",
21
- },
22
- } as MinimalClerkClient;
23
-
24
- expect(getClerkUsername(mockClerk)).toBe("John Doe");
13
+ } as ClerkUser),
14
+ ).toBe("John Doe");
25
15
  });
26
16
 
27
17
  it("should return firstName + lastName if available and no fullName", () => {
28
- const mockClerk = {
29
- user: {
18
+ expect(
19
+ getClerkUsername({
30
20
  firstName: "John",
31
21
  lastName: "Doe",
32
22
  username: "johndoe",
33
- },
34
- } as MinimalClerkClient;
35
-
36
- expect(getClerkUsername(mockClerk)).toBe("John Doe");
23
+ } as ClerkUser),
24
+ ).toBe("John Doe");
37
25
  });
38
26
 
39
27
  it("should return firstName if available and no lastName or fullName", () => {
40
- const mockClerk = {
41
- user: {
28
+ expect(
29
+ getClerkUsername({
42
30
  firstName: "John",
43
31
  username: "johndoe",
44
- },
45
- } as MinimalClerkClient;
46
-
47
- expect(getClerkUsername(mockClerk)).toBe("John");
32
+ } as ClerkUser),
33
+ ).toBe("John");
48
34
  });
49
35
 
50
36
  it("should return username if available and no names", () => {
51
- const mockClerk = {
52
- user: {
37
+ expect(
38
+ getClerkUsername({
53
39
  username: "johndoe",
54
- },
55
- } as MinimalClerkClient;
56
-
57
- expect(getClerkUsername(mockClerk)).toBe("johndoe");
40
+ } as ClerkUser),
41
+ ).toBe("johndoe");
58
42
  });
59
43
 
60
44
  it("should return email username if available and no other identifiers", () => {
61
- const mockClerk = {
62
- user: {
45
+ expect(
46
+ getClerkUsername({
63
47
  primaryEmailAddress: {
64
48
  emailAddress: "john.doe@example.com",
65
49
  },
66
- },
67
- } as MinimalClerkClient;
68
-
69
- expect(getClerkUsername(mockClerk)).toBe("john.doe");
50
+ } as ClerkUser),
51
+ ).toBe("john.doe");
70
52
  });
71
53
 
72
54
  it("should return user id as last resort", () => {
73
- const mockClerk = {
74
- user: {
55
+ expect(
56
+ getClerkUsername({
75
57
  id: "user_123",
76
- },
77
- } as MinimalClerkClient;
78
-
79
- expect(getClerkUsername(mockClerk)).toBe("user_123");
58
+ } as ClerkUser),
59
+ ).toBe("user_123");
80
60
  });
81
61
  });
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { isClerkAuthStateEqual } from "../types";
2
+ import { ClerkUser, isClerkAuthStateEqual } from "../types";
3
3
 
4
4
  describe("isClerkAuthStateEqual", () => {
5
5
  const validCredentials = {
@@ -91,7 +91,9 @@ describe("isClerkAuthStateEqual", () => {
91
91
  description: "both have incomplete credentials",
92
92
  },
93
93
  ])("returns true when $description", ({ previous, next }) => {
94
- expect(isClerkAuthStateEqual(previous, next)).toBe(true);
94
+ expect(
95
+ isClerkAuthStateEqual(previous as ClerkUser, next as ClerkUser),
96
+ ).toBe(true);
95
97
  });
96
98
  });
97
99
 
@@ -118,7 +120,9 @@ describe("isClerkAuthStateEqual", () => {
118
120
  description: "previous has incomplete, next has credentials",
119
121
  },
120
122
  ])("returns false when $description", ({ previous, next }) => {
121
- expect(isClerkAuthStateEqual(previous, next)).toBe(false);
123
+ expect(
124
+ isClerkAuthStateEqual(previous as ClerkUser, next as ClerkUser),
125
+ ).toBe(false);
122
126
  });
123
127
  });
124
128
  });
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { isClerkCredentials } from "../types";
2
+ import { ClerkUser, isClerkCredentials } from "../types";
3
3
 
4
4
  describe("isClerkCredentials", () => {
5
5
  it.each([
@@ -44,6 +44,8 @@ describe("isClerkCredentials", () => {
44
44
  description: "missing jazzAccountSecret",
45
45
  },
46
46
  ])("fails for invalid credentials: $description", ({ metadata }) => {
47
- expect(isClerkCredentials(metadata)).toBe(false);
47
+ expect(isClerkCredentials(metadata as ClerkUser["unsafeMetadata"])).toBe(
48
+ false,
49
+ );
48
50
  });
49
51
  });
@@ -1,32 +1,57 @@
1
- import { AgentSecret } from "cojson";
2
- import { Account, ID } from "jazz-tools";
1
+ import { type AgentSecret } from "cojson";
2
+ import { z } from "zod/v4";
3
+
4
+ const ClerkJazzCredentialsSchema = z.object({
5
+ jazzAccountID: z.string(),
6
+ jazzAccountSecret: z.string(),
7
+ jazzAccountSeed: z.array(z.number()).optional(),
8
+ });
9
+
10
+ const ClerkUserSchema = z.object({
11
+ fullName: z.string().nullish(),
12
+ username: z.string().nullish(),
13
+ firstName: z.string().nullish(),
14
+ lastName: z.string().nullish(),
15
+ id: z.string().optional(),
16
+ primaryEmailAddress: z
17
+ .object({
18
+ emailAddress: z.string().nullable(),
19
+ })
20
+ .nullish(),
21
+ unsafeMetadata: z.union([z.object({}), ClerkJazzCredentialsSchema]),
22
+ update: z.function({
23
+ input: [
24
+ z.object({
25
+ unsafeMetadata: ClerkJazzCredentialsSchema,
26
+ }),
27
+ ],
28
+ output: z.promise(z.unknown()),
29
+ }),
30
+ });
31
+
32
+ export const ClerkEventSchema = z.object({
33
+ user: ClerkUserSchema.nullish(),
34
+ });
35
+ export type ClerkEventSchema = z.infer<typeof ClerkEventSchema>;
36
+
37
+ export type ClerkUser = z.infer<typeof ClerkUserSchema>;
38
+
39
+ // Need to provide a permissive type externally to accept
40
+ type PermissiveClerkUser = Omit<ClerkUser, "unsafeMetadata" | "update"> & {
41
+ unsafeMetadata: Record<string, unknown>;
42
+ update: (args: {
43
+ unsafeMetadata: Record<string, unknown>;
44
+ }) => Promise<unknown>;
45
+ };
3
46
 
4
47
  export type MinimalClerkClient = {
5
- user:
6
- | {
7
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
- unsafeMetadata: Record<string, any>;
9
- fullName: string | null;
10
- username: string | null;
11
- firstName: string | null;
12
- lastName: string | null;
13
- id: string;
14
- primaryEmailAddress: {
15
- emailAddress: string | null;
16
- } | null;
17
- update: (args: {
18
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
- unsafeMetadata: Record<string, any>;
20
- }) => Promise<unknown>;
21
- }
22
- | null
23
- | undefined;
48
+ user: PermissiveClerkUser | null | undefined;
24
49
  signOut: () => Promise<void>;
25
50
  addListener: (listener: (data: unknown) => void) => void;
26
51
  };
27
52
 
28
53
  export type ClerkCredentials = {
29
- jazzAccountID: ID<Account>;
54
+ jazzAccountID: string;
30
55
  jazzAccountSecret: AgentSecret;
31
56
  jazzAccountSeed?: number[];
32
57
  };
@@ -36,23 +61,19 @@ export type ClerkCredentials = {
36
61
  * **Note**: It does not validate the credentials, only checks if the necessary fields are present in the metadata object.
37
62
  */
38
63
  export function isClerkCredentials(
39
- data:
40
- | NonNullable<MinimalClerkClient["user"]>["unsafeMetadata"]
41
- | null
42
- | undefined,
64
+ data: Record<string, unknown> | undefined,
43
65
  ): data is ClerkCredentials {
44
66
  return !!data && "jazzAccountID" in data && "jazzAccountSecret" in data;
45
67
  }
46
68
 
69
+ type ClerkUserWithUnsafeMetadata =
70
+ | Pick<ClerkUser, "unsafeMetadata">
71
+ | null
72
+ | undefined;
73
+
47
74
  export function isClerkAuthStateEqual(
48
- previousUser:
49
- | Pick<NonNullable<MinimalClerkClient["user"]>, "unsafeMetadata">
50
- | null
51
- | undefined,
52
- newUser:
53
- | Pick<NonNullable<MinimalClerkClient["user"]>, "unsafeMetadata">
54
- | null
55
- | undefined,
75
+ previousUser: ClerkUserWithUnsafeMetadata,
76
+ newUser: ClerkUserWithUnsafeMetadata,
56
77
  ) {
57
78
  if (Boolean(previousUser) !== Boolean(newUser)) {
58
79
  return false;
@@ -188,24 +188,45 @@ export class JazzContextManager<
188
188
  return this.subscriptionCache;
189
189
  }
190
190
 
191
+ /**
192
+ * Flag to indicate if a logout operation is currently in progress.
193
+ * Used to prevent concurrent logout attempts or double-logout issues.
194
+ * Set to true when logout starts, reset to false once all logout logic runs.
195
+ */
196
+ loggingOut = false;
197
+
198
+ /**
199
+ * Handles the logout process.
200
+ * Uses the loggingOut flag to ensure only one logout can happen at a time.
201
+ */
191
202
  logOut = async () => {
192
- if (!this.context || !this.props) {
203
+ if (!this.context || !this.props || this.loggingOut) {
193
204
  return;
194
205
  }
195
206
 
207
+ // Mark as logging out to prevent reentry
208
+ this.loggingOut = true;
209
+
196
210
  this.authenticatingAccountID = null;
197
211
 
198
212
  // Clear cache on logout to prevent subscription leaks across authentication boundaries
199
213
  this.subscriptionCache.clear();
200
214
 
201
- await this.props.onLogOut?.();
215
+ try {
216
+ await this.props.onLogOut?.();
202
217
 
203
- if (this.props.logOutReplacement) {
204
- await this.props.logOutReplacement();
205
- } else {
206
- await this.context.logOut();
207
- return this.createContext(this.props);
218
+ if (this.props.logOutReplacement) {
219
+ await this.props.logOutReplacement();
220
+ } else {
221
+ await this.context.logOut();
222
+ await this.createContext(this.props);
223
+ }
224
+ } catch (error) {
225
+ console.error("Error during logout", error);
208
226
  }
227
+
228
+ // Reset flag after standard logout finishes
229
+ this.loggingOut = false;
209
230
  };
210
231
 
211
232
  done = () => {
@@ -604,6 +604,22 @@ describe("ContextManager", () => {
604
604
  expect(onAnonymousAccountDiscarded).toHaveBeenCalledTimes(1);
605
605
  });
606
606
 
607
+ test("prevents concurrent logout attempts", async () => {
608
+ const onLogOut = vi.fn();
609
+ await manager.createContext({ onLogOut });
610
+
611
+ // Start multiple concurrent logout attempts
612
+ const promises = [];
613
+ for (let i = 0; i < 5; i++) {
614
+ promises.push(manager.logOut());
615
+ }
616
+
617
+ await Promise.all(promises);
618
+
619
+ // onLogOut should only be called once despite multiple logOut calls
620
+ expect(onLogOut).toHaveBeenCalledTimes(1);
621
+ });
622
+
607
623
  test("allows authentication after logout", async () => {
608
624
  const account = await createJazzTestAccount();
609
625
  const onAnonymousAccountDiscarded = vi.fn();
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=types.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.test.d.ts","sourceRoot":"","sources":["../../../../../src/tools/auth/clerk/tests/types.test.ts"],"names":[],"mappings":""}