jazz-tools 0.19.15 → 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.
- package/.svelte-kit/__package__/tests/media/image.svelte.test.js +4 -1
- package/.turbo/turbo-build.log +60 -60
- package/CHANGELOG.md +19 -0
- package/dist/{chunk-R3KIZG4P.js → chunk-OH2GW5WP.js} +24 -8
- package/dist/{chunk-R3KIZG4P.js.map → chunk-OH2GW5WP.js.map} +1 -1
- package/dist/index.js +86 -54
- package/dist/index.js.map +1 -1
- package/dist/react-native/index.js +18 -17
- package/dist/react-native/index.js.map +1 -1
- package/dist/react-native-core/ReactNativeSessionProvider.d.ts.map +1 -1
- package/dist/react-native-core/index.js +18 -17
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/svelte/tests/media/image.svelte.test.js +4 -1
- package/dist/testing.js +1 -1
- package/dist/tools/auth/clerk/getClerkUsername.d.ts +2 -2
- package/dist/tools/auth/clerk/getClerkUsername.d.ts.map +1 -1
- package/dist/tools/auth/clerk/index.d.ts +5 -4
- package/dist/tools/auth/clerk/index.d.ts.map +1 -1
- package/dist/tools/auth/clerk/tests/isClerkAuthStateEqual.test.d.ts +2 -0
- package/dist/tools/auth/clerk/tests/isClerkAuthStateEqual.test.d.ts.map +1 -0
- package/dist/tools/auth/clerk/tests/isClerkCredentials.test.d.ts +2 -0
- package/dist/tools/auth/clerk/tests/isClerkCredentials.test.d.ts.map +1 -0
- package/dist/tools/auth/clerk/types.d.ts +62 -19
- package/dist/tools/auth/clerk/types.d.ts.map +1 -1
- package/dist/tools/implementation/ContextManager.d.ts +10 -0
- package/dist/tools/implementation/ContextManager.d.ts.map +1 -1
- package/package.json +5 -6
- package/src/react/tests/media/image.test.tsx +5 -1
- package/src/react-native-core/ReactNativeSessionProvider.ts +24 -24
- package/src/svelte/tests/media/image.svelte.test.ts +5 -1
- package/src/tools/auth/clerk/getClerkUsername.ts +13 -20
- package/src/tools/auth/clerk/index.ts +35 -28
- package/src/tools/auth/clerk/tests/JazzClerkAuth.test.ts +105 -33
- package/src/tools/auth/clerk/tests/getClerkUsername.test.ts +25 -45
- package/src/tools/auth/clerk/tests/isClerkAuthStateEqual.test.ts +128 -0
- package/src/tools/auth/clerk/tests/{types.test.ts → isClerkCredentials.test.ts} +4 -2
- package/src/tools/auth/clerk/types.ts +66 -28
- package/src/tools/implementation/ContextManager.ts +28 -7
- package/src/tools/tests/ContextManager.test.ts +16 -0
- package/dist/tools/auth/clerk/tests/types.test.d.ts +0 -2
- package/dist/tools/auth/clerk/tests/types.test.d.ts.map +0 -1
|
@@ -6,7 +6,7 @@ import { Account, ID, InMemoryKVStore, KvStoreContext } from "jazz-tools";
|
|
|
6
6
|
import { createJazzTestAccount } from "jazz-tools/testing";
|
|
7
7
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
8
|
import { JazzClerkAuth } from "../index";
|
|
9
|
-
import type { MinimalClerkClient } from "../types";
|
|
9
|
+
import type { ClerkEventSchema, ClerkUser, MinimalClerkClient } from "../types";
|
|
10
10
|
|
|
11
11
|
KvStoreContext.getInstance().initialize(new InMemoryKVStore());
|
|
12
12
|
const authSecretStorage = new AuthSecretStorage();
|
|
@@ -27,23 +27,16 @@ describe("JazzClerkAuth", () => {
|
|
|
27
27
|
|
|
28
28
|
describe("onClerkUserChange", () => {
|
|
29
29
|
it("should do nothing if no clerk user", async () => {
|
|
30
|
-
|
|
31
|
-
user: null,
|
|
32
|
-
} as MinimalClerkClient;
|
|
33
|
-
|
|
34
|
-
await auth.onClerkUserChange(mockClerk);
|
|
30
|
+
await auth.onClerkUserChange(null);
|
|
35
31
|
expect(mockAuthenticate).not.toHaveBeenCalled();
|
|
36
32
|
});
|
|
37
33
|
|
|
38
34
|
it("should throw if not authenticated locally", async () => {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
},
|
|
43
|
-
signOut: vi.fn(),
|
|
44
|
-
} as unknown as MinimalClerkClient;
|
|
35
|
+
const user = {
|
|
36
|
+
unsafeMetadata: {},
|
|
37
|
+
} as ClerkUser;
|
|
45
38
|
|
|
46
|
-
await expect(auth.onClerkUserChange(
|
|
39
|
+
await expect(auth.onClerkUserChange(user)).rejects.toThrow();
|
|
47
40
|
expect(mockAuthenticate).not.toHaveBeenCalled();
|
|
48
41
|
});
|
|
49
42
|
|
|
@@ -61,11 +54,10 @@ describe("JazzClerkAuth", () => {
|
|
|
61
54
|
fullName: "Guido",
|
|
62
55
|
unsafeMetadata: {},
|
|
63
56
|
update: vi.fn(),
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
} as unknown as MinimalClerkClient;
|
|
57
|
+
} as ClerkUser,
|
|
58
|
+
};
|
|
67
59
|
|
|
68
|
-
await auth.onClerkUserChange(mockClerk);
|
|
60
|
+
await auth.onClerkUserChange(mockClerk.user);
|
|
69
61
|
|
|
70
62
|
expect(mockClerk.user?.update).toHaveBeenCalledWith({
|
|
71
63
|
unsafeMetadata: {
|
|
@@ -106,11 +98,11 @@ describe("JazzClerkAuth", () => {
|
|
|
106
98
|
jazzAccountSecret: "secret123",
|
|
107
99
|
jazzAccountSeed: [1, 2, 3],
|
|
108
100
|
},
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
101
|
+
update: vi.fn(),
|
|
102
|
+
} as ClerkUser,
|
|
103
|
+
};
|
|
112
104
|
|
|
113
|
-
await auth.onClerkUserChange(mockClerk);
|
|
105
|
+
await auth.onClerkUserChange(mockClerk.user);
|
|
114
106
|
|
|
115
107
|
expect(mockAuthenticate).toHaveBeenCalledWith({
|
|
116
108
|
accountID: "test123",
|
|
@@ -137,13 +129,12 @@ describe("JazzClerkAuth", () => {
|
|
|
137
129
|
jazzAccountSecret: "secret123",
|
|
138
130
|
jazzAccountSeed: [1, 2, 3],
|
|
139
131
|
},
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
await auth.onClerkUserChange(mockClerk);
|
|
132
|
+
update: vi.fn(),
|
|
133
|
+
} as ClerkUser,
|
|
134
|
+
};
|
|
145
135
|
|
|
146
|
-
await auth.onClerkUserChange(
|
|
136
|
+
await auth.onClerkUserChange(mockClerk.user);
|
|
137
|
+
await auth.onClerkUserChange(null);
|
|
147
138
|
|
|
148
139
|
expect(authSecretStorage.isAuthenticated).toBe(false);
|
|
149
140
|
expect(mockLogOut).toHaveBeenCalled();
|
|
@@ -151,10 +142,8 @@ describe("JazzClerkAuth", () => {
|
|
|
151
142
|
});
|
|
152
143
|
|
|
153
144
|
describe("registerListener", () => {
|
|
154
|
-
function setupMockClerk(user:
|
|
155
|
-
const listners = new Set<
|
|
156
|
-
(clerkClient: Pick<MinimalClerkClient, "user">) => void
|
|
157
|
-
>();
|
|
145
|
+
function setupMockClerk(user: ClerkUser | null) {
|
|
146
|
+
const listners = new Set<(clerkClient: ClerkEventSchema) => void>();
|
|
158
147
|
|
|
159
148
|
return {
|
|
160
149
|
client: {
|
|
@@ -166,9 +155,9 @@ describe("JazzClerkAuth", () => {
|
|
|
166
155
|
};
|
|
167
156
|
}),
|
|
168
157
|
} as unknown as MinimalClerkClient,
|
|
169
|
-
triggerUserChange: (user:
|
|
158
|
+
triggerUserChange: (user: ClerkUser | null | undefined) => {
|
|
170
159
|
for (const listener of listners) {
|
|
171
|
-
listener({ user }
|
|
160
|
+
listener({ user });
|
|
172
161
|
}
|
|
173
162
|
},
|
|
174
163
|
};
|
|
@@ -211,6 +200,7 @@ describe("JazzClerkAuth", () => {
|
|
|
211
200
|
jazzAccountSecret: "secret123",
|
|
212
201
|
jazzAccountSeed: [1, 2, 3],
|
|
213
202
|
},
|
|
203
|
+
update: vi.fn(),
|
|
214
204
|
});
|
|
215
205
|
|
|
216
206
|
expect(onClerkUserChangeSpy).toHaveBeenCalledTimes(2);
|
|
@@ -251,6 +241,7 @@ describe("JazzClerkAuth", () => {
|
|
|
251
241
|
jazzAccountSecret: "secret123",
|
|
252
242
|
jazzAccountSeed: [1, 2, 3],
|
|
253
243
|
},
|
|
244
|
+
update: vi.fn(),
|
|
254
245
|
});
|
|
255
246
|
|
|
256
247
|
triggerUserChange({
|
|
@@ -259,6 +250,7 @@ describe("JazzClerkAuth", () => {
|
|
|
259
250
|
jazzAccountSecret: "secret123",
|
|
260
251
|
jazzAccountSeed: [1, 2, 3],
|
|
261
252
|
},
|
|
253
|
+
update: vi.fn(),
|
|
262
254
|
});
|
|
263
255
|
|
|
264
256
|
expect(onClerkUserChangeSpy).toHaveBeenCalledTimes(1);
|
|
@@ -282,6 +274,86 @@ describe("JazzClerkAuth", () => {
|
|
|
282
274
|
|
|
283
275
|
expect(onClerkUserChangeSpy).toHaveBeenCalledTimes(1);
|
|
284
276
|
});
|
|
277
|
+
|
|
278
|
+
it("should complete signup flow when new Clerk user is detected", async () => {
|
|
279
|
+
// 1. Setup local credentials (simulating anonymous user)
|
|
280
|
+
await authSecretStorage.set({
|
|
281
|
+
accountID: "test-account-id" as ID<Account>,
|
|
282
|
+
secretSeed: new Uint8Array([1, 2, 3]),
|
|
283
|
+
accountSecret: "test-secret" as AgentSecret,
|
|
284
|
+
provider: "anonymous",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const { client, triggerUserChange } = setupMockClerk(null);
|
|
288
|
+
|
|
289
|
+
const auth = new JazzClerkAuth(
|
|
290
|
+
mockAuthenticate,
|
|
291
|
+
mockLogOut,
|
|
292
|
+
authSecretStorage,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// 2. Register listener with null user (no one logged in yet)
|
|
296
|
+
auth.registerListener(client);
|
|
297
|
+
|
|
298
|
+
// Initial trigger with no user
|
|
299
|
+
triggerUserChange(null);
|
|
300
|
+
|
|
301
|
+
// 3. Trigger event with new Clerk user (no Jazz credentials yet)
|
|
302
|
+
const mockUserUpdate = vi.fn((data) => {
|
|
303
|
+
triggerUserChange({
|
|
304
|
+
...data,
|
|
305
|
+
update: mockUserUpdate,
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const signInSpy = vi.spyOn(auth, "signIn");
|
|
310
|
+
const logInSpy = vi.spyOn(auth, "logIn");
|
|
311
|
+
|
|
312
|
+
const newClerkUser = {
|
|
313
|
+
fullName: "Test User",
|
|
314
|
+
firstName: "Test",
|
|
315
|
+
lastName: "User",
|
|
316
|
+
username: "testuser",
|
|
317
|
+
id: "clerk-user-123",
|
|
318
|
+
primaryEmailAddress: { emailAddress: "test@example.com" },
|
|
319
|
+
unsafeMetadata: {}, // No Jazz credentials yet
|
|
320
|
+
update: mockUserUpdate,
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
triggerUserChange(newClerkUser);
|
|
324
|
+
|
|
325
|
+
// Wait for async operations to complete
|
|
326
|
+
await vi.waitFor(() => {
|
|
327
|
+
expect(mockUserUpdate).toHaveBeenCalled();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// 4. Verify credentials synced to Clerk
|
|
331
|
+
expect(mockUserUpdate).toHaveBeenCalledWith({
|
|
332
|
+
unsafeMetadata: {
|
|
333
|
+
jazzAccountID: "test-account-id",
|
|
334
|
+
jazzAccountSecret: "test-secret",
|
|
335
|
+
jazzAccountSeed: [1, 2, 3],
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Verify profile name was updated from Clerk username
|
|
340
|
+
const me = await Account.getMe().$jazz.ensureLoaded({
|
|
341
|
+
resolve: { profile: true },
|
|
342
|
+
});
|
|
343
|
+
expect(me.profile.name).toBe("Test User");
|
|
344
|
+
|
|
345
|
+
// Verify authSecretStorage is updated with provider "clerk"
|
|
346
|
+
const storedCredentials = await authSecretStorage.get();
|
|
347
|
+
expect(storedCredentials).toEqual({
|
|
348
|
+
accountID: "test-account-id",
|
|
349
|
+
accountSecret: "test-secret",
|
|
350
|
+
secretSeed: new Uint8Array([1, 2, 3]),
|
|
351
|
+
provider: "clerk",
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
expect(signInSpy).toHaveBeenCalled();
|
|
355
|
+
expect(logInSpy).not.toHaveBeenCalled();
|
|
356
|
+
});
|
|
285
357
|
});
|
|
286
358
|
|
|
287
359
|
describe("initializeAuth", () => {
|
|
@@ -1,81 +1,61 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { getClerkUsername } from "../getClerkUsername.js";
|
|
3
|
-
import type {
|
|
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
|
-
|
|
16
|
-
|
|
7
|
+
expect(
|
|
8
|
+
getClerkUsername({
|
|
17
9
|
fullName: "John Doe",
|
|
18
10
|
firstName: "John",
|
|
19
11
|
lastName: "Doe",
|
|
20
12
|
username: "johndoe",
|
|
21
|
-
},
|
|
22
|
-
|
|
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
|
-
|
|
29
|
-
|
|
18
|
+
expect(
|
|
19
|
+
getClerkUsername({
|
|
30
20
|
firstName: "John",
|
|
31
21
|
lastName: "Doe",
|
|
32
22
|
username: "johndoe",
|
|
33
|
-
},
|
|
34
|
-
|
|
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
|
-
|
|
41
|
-
|
|
28
|
+
expect(
|
|
29
|
+
getClerkUsername({
|
|
42
30
|
firstName: "John",
|
|
43
31
|
username: "johndoe",
|
|
44
|
-
},
|
|
45
|
-
|
|
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
|
-
|
|
52
|
-
|
|
37
|
+
expect(
|
|
38
|
+
getClerkUsername({
|
|
53
39
|
username: "johndoe",
|
|
54
|
-
},
|
|
55
|
-
|
|
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
|
-
|
|
62
|
-
|
|
45
|
+
expect(
|
|
46
|
+
getClerkUsername({
|
|
63
47
|
primaryEmailAddress: {
|
|
64
48
|
emailAddress: "john.doe@example.com",
|
|
65
49
|
},
|
|
66
|
-
},
|
|
67
|
-
|
|
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
|
-
|
|
74
|
-
|
|
55
|
+
expect(
|
|
56
|
+
getClerkUsername({
|
|
75
57
|
id: "user_123",
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
expect(getClerkUsername(mockClerk)).toBe("user_123");
|
|
58
|
+
} as ClerkUser),
|
|
59
|
+
).toBe("user_123");
|
|
80
60
|
});
|
|
81
61
|
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ClerkUser, isClerkAuthStateEqual } from "../types";
|
|
3
|
+
|
|
4
|
+
describe("isClerkAuthStateEqual", () => {
|
|
5
|
+
const validCredentials = {
|
|
6
|
+
jazzAccountID: "account-123",
|
|
7
|
+
jazzAccountSecret: "secret-123",
|
|
8
|
+
jazzAccountSeed: [1, 2, 3],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const differentCredentials = {
|
|
12
|
+
jazzAccountID: "account-456",
|
|
13
|
+
jazzAccountSecret: "secret-456",
|
|
14
|
+
jazzAccountSeed: [4, 5, 6],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe("both users null/undefined", () => {
|
|
18
|
+
it.each([
|
|
19
|
+
{ previous: null, next: null, description: "both null" },
|
|
20
|
+
{ previous: undefined, next: undefined, description: "both undefined" },
|
|
21
|
+
{ previous: null, next: undefined, description: "null and undefined" },
|
|
22
|
+
{ previous: undefined, next: null, description: "undefined and null" },
|
|
23
|
+
])("returns true when $description", ({ previous, next }) => {
|
|
24
|
+
expect(isClerkAuthStateEqual(previous, next)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("one user null, other exists", () => {
|
|
29
|
+
it.each([
|
|
30
|
+
{
|
|
31
|
+
previous: null,
|
|
32
|
+
next: { unsafeMetadata: validCredentials },
|
|
33
|
+
description: "previous null, next exists",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
previous: { unsafeMetadata: validCredentials },
|
|
37
|
+
next: null,
|
|
38
|
+
description: "previous exists, next null",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
previous: undefined,
|
|
42
|
+
next: { unsafeMetadata: validCredentials },
|
|
43
|
+
description: "previous undefined, next exists",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
previous: { unsafeMetadata: validCredentials },
|
|
47
|
+
next: undefined,
|
|
48
|
+
description: "previous exists, next undefined",
|
|
49
|
+
},
|
|
50
|
+
])("returns false when $description", ({ previous, next }) => {
|
|
51
|
+
expect(isClerkAuthStateEqual(previous, next)).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("same jazzAccountID", () => {
|
|
56
|
+
it("returns true when both users have the same jazzAccountID", () => {
|
|
57
|
+
const previous = { unsafeMetadata: validCredentials };
|
|
58
|
+
const next = {
|
|
59
|
+
unsafeMetadata: {
|
|
60
|
+
...validCredentials,
|
|
61
|
+
jazzAccountSecret: "different-secret",
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
expect(isClerkAuthStateEqual(previous, next)).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("different jazzAccountID", () => {
|
|
69
|
+
it("returns false when users have different jazzAccountID", () => {
|
|
70
|
+
const previous = { unsafeMetadata: validCredentials };
|
|
71
|
+
const next = { unsafeMetadata: differentCredentials };
|
|
72
|
+
expect(isClerkAuthStateEqual(previous, next)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("neither user has valid credentials", () => {
|
|
77
|
+
it.each([
|
|
78
|
+
{
|
|
79
|
+
previous: { unsafeMetadata: {} },
|
|
80
|
+
next: { unsafeMetadata: {} },
|
|
81
|
+
description: "both have empty metadata",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
previous: { unsafeMetadata: { someOtherField: "value" } },
|
|
85
|
+
next: { unsafeMetadata: { anotherField: "value" } },
|
|
86
|
+
description: "both have non-credential metadata",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
previous: { unsafeMetadata: { jazzAccountID: "123" } },
|
|
90
|
+
next: { unsafeMetadata: { jazzAccountSecret: "456" } },
|
|
91
|
+
description: "both have incomplete credentials",
|
|
92
|
+
},
|
|
93
|
+
])("returns true when $description", ({ previous, next }) => {
|
|
94
|
+
expect(
|
|
95
|
+
isClerkAuthStateEqual(previous as ClerkUser, next as ClerkUser),
|
|
96
|
+
).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("one has credentials, other doesn't", () => {
|
|
101
|
+
it.each([
|
|
102
|
+
{
|
|
103
|
+
previous: { unsafeMetadata: validCredentials },
|
|
104
|
+
next: { unsafeMetadata: {} },
|
|
105
|
+
description: "previous has credentials, next empty",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
previous: { unsafeMetadata: {} },
|
|
109
|
+
next: { unsafeMetadata: validCredentials },
|
|
110
|
+
description: "previous empty, next has credentials",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
previous: { unsafeMetadata: validCredentials },
|
|
114
|
+
next: { unsafeMetadata: { jazzAccountID: "123" } },
|
|
115
|
+
description: "previous has credentials, next has incomplete",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
previous: { unsafeMetadata: { jazzAccountSecret: "456" } },
|
|
119
|
+
next: { unsafeMetadata: validCredentials },
|
|
120
|
+
description: "previous has incomplete, next has credentials",
|
|
121
|
+
},
|
|
122
|
+
])("returns false when $description", ({ previous, next }) => {
|
|
123
|
+
expect(
|
|
124
|
+
isClerkAuthStateEqual(previous as ClerkUser, next as ClerkUser),
|
|
125
|
+
).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
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(
|
|
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 {
|
|
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:
|
|
54
|
+
jazzAccountID: string;
|
|
30
55
|
jazzAccountSecret: AgentSecret;
|
|
31
56
|
jazzAccountSeed?: number[];
|
|
32
57
|
};
|
|
@@ -36,21 +61,34 @@ 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:
|
|
64
|
+
data: Record<string, unknown> | undefined,
|
|
40
65
|
): data is ClerkCredentials {
|
|
41
66
|
return !!data && "jazzAccountID" in data && "jazzAccountSecret" in data;
|
|
42
67
|
}
|
|
43
68
|
|
|
69
|
+
type ClerkUserWithUnsafeMetadata =
|
|
70
|
+
| Pick<ClerkUser, "unsafeMetadata">
|
|
71
|
+
| null
|
|
72
|
+
| undefined;
|
|
73
|
+
|
|
44
74
|
export function isClerkAuthStateEqual(
|
|
45
|
-
previousUser:
|
|
46
|
-
newUser:
|
|
75
|
+
previousUser: ClerkUserWithUnsafeMetadata,
|
|
76
|
+
newUser: ClerkUserWithUnsafeMetadata,
|
|
47
77
|
) {
|
|
48
78
|
if (Boolean(previousUser) !== Boolean(newUser)) {
|
|
49
79
|
return false;
|
|
50
80
|
}
|
|
51
81
|
|
|
52
|
-
const previousCredentials = isClerkCredentials(previousUser?.unsafeMetadata)
|
|
53
|
-
|
|
82
|
+
const previousCredentials = isClerkCredentials(previousUser?.unsafeMetadata)
|
|
83
|
+
? previousUser?.unsafeMetadata
|
|
84
|
+
: null;
|
|
85
|
+
const newCredentials = isClerkCredentials(newUser?.unsafeMetadata)
|
|
86
|
+
? newUser?.unsafeMetadata
|
|
87
|
+
: null;
|
|
88
|
+
|
|
89
|
+
if (!previousCredentials || !newCredentials) {
|
|
90
|
+
return previousCredentials === newCredentials;
|
|
91
|
+
}
|
|
54
92
|
|
|
55
|
-
return previousCredentials === newCredentials;
|
|
93
|
+
return previousCredentials.jazzAccountID === newCredentials.jazzAccountID;
|
|
56
94
|
}
|
|
@@ -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
|
-
|
|
215
|
+
try {
|
|
216
|
+
await this.props.onLogOut?.();
|
|
202
217
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.test.d.ts","sourceRoot":"","sources":["../../../../../src/tools/auth/clerk/tests/types.test.ts"],"names":[],"mappings":""}
|