jazz-tools 0.19.19 → 0.19.21

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 (127) hide show
  1. package/.svelte-kit/__package__/client.d.ts.map +1 -1
  2. package/.svelte-kit/__package__/client.js +3 -1
  3. package/.svelte-kit/__package__/server.d.ts.map +1 -1
  4. package/.svelte-kit/__package__/server.js +9 -7
  5. package/.svelte-kit/__package__/tests/client.test.js +48 -0
  6. package/.turbo/turbo-build.log +70 -66
  7. package/dist/better-auth/auth/client.d.ts.map +1 -1
  8. package/dist/better-auth/auth/client.js +1 -1
  9. package/dist/better-auth/auth/client.js.map +1 -1
  10. package/dist/better-auth/auth/server.d.ts.map +1 -1
  11. package/dist/better-auth/auth/server.js +4 -4
  12. package/dist/better-auth/auth/server.js.map +1 -1
  13. package/dist/better-auth/database-adapter/index.js.map +1 -1
  14. package/dist/better-auth/database-adapter/repository/generic.d.ts +3 -3
  15. package/dist/better-auth/database-adapter/repository/session.d.ts +2 -2
  16. package/dist/better-auth/database-adapter/schema.d.ts +3 -3
  17. package/dist/better-auth/database-adapter/schema.d.ts.map +1 -1
  18. package/dist/{chunk-PEHQ7TN2.js → chunk-QCTQH5RS.js} +31 -4
  19. package/dist/chunk-QCTQH5RS.js.map +1 -0
  20. package/dist/index.js +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/react/hooks.d.ts +1 -2
  23. package/dist/react/hooks.d.ts.map +1 -1
  24. package/dist/react/index.js +7 -2
  25. package/dist/react/index.js.map +1 -1
  26. package/dist/react-core/hooks.d.ts +94 -3
  27. package/dist/react-core/hooks.d.ts.map +1 -1
  28. package/dist/react-core/index.js +130 -135
  29. package/dist/react-core/index.js.map +1 -1
  30. package/dist/react-core/tests/useCoStates.test.d.ts +2 -0
  31. package/dist/react-core/tests/useCoStates.test.d.ts.map +1 -0
  32. package/dist/react-native/chunk-DGUM43GV.js +11 -0
  33. package/dist/react-native/chunk-DGUM43GV.js.map +1 -0
  34. package/dist/react-native/crypto.js +2 -0
  35. package/dist/react-native/crypto.js.map +1 -1
  36. package/dist/react-native/index.js +544 -29
  37. package/dist/react-native/index.js.map +1 -1
  38. package/dist/react-native-core/auth/PasskeyAuth.d.ts +123 -0
  39. package/dist/react-native-core/auth/PasskeyAuth.d.ts.map +1 -0
  40. package/dist/react-native-core/auth/PasskeyAuthBasicUI.d.ts +34 -0
  41. package/dist/react-native-core/auth/PasskeyAuthBasicUI.d.ts.map +1 -0
  42. package/dist/react-native-core/auth/auth.d.ts +3 -0
  43. package/dist/react-native-core/auth/auth.d.ts.map +1 -1
  44. package/dist/react-native-core/auth/passkey-utils.d.ts +16 -0
  45. package/dist/react-native-core/auth/passkey-utils.d.ts.map +1 -0
  46. package/dist/react-native-core/auth/usePasskeyAuth.d.ts +48 -0
  47. package/dist/react-native-core/auth/usePasskeyAuth.d.ts.map +1 -0
  48. package/dist/react-native-core/chunk-DGUM43GV.js +11 -0
  49. package/dist/react-native-core/chunk-DGUM43GV.js.map +1 -0
  50. package/dist/react-native-core/crypto.js +2 -0
  51. package/dist/react-native-core/crypto.js.map +1 -1
  52. package/dist/react-native-core/hooks.d.ts +1 -1
  53. package/dist/react-native-core/hooks.d.ts.map +1 -1
  54. package/dist/react-native-core/index.js +539 -24
  55. package/dist/react-native-core/index.js.map +1 -1
  56. package/dist/react-native-core/tests/PasskeyAuth.test.d.ts +2 -0
  57. package/dist/react-native-core/tests/PasskeyAuth.test.d.ts.map +1 -0
  58. package/dist/react-native-core/tests/passkey-utils.test.d.ts +2 -0
  59. package/dist/react-native-core/tests/passkey-utils.test.d.ts.map +1 -0
  60. package/dist/svelte/auth/ClerkAuth.svelte.d.ts +38 -0
  61. package/dist/svelte/auth/ClerkAuth.svelte.d.ts.map +1 -0
  62. package/dist/svelte/auth/ClerkAuth.svelte.js +47 -0
  63. package/dist/svelte/auth/JazzSvelteProviderWithClerk.svelte +156 -0
  64. package/dist/svelte/auth/JazzSvelteProviderWithClerk.svelte.d.ts +67 -0
  65. package/dist/svelte/auth/JazzSvelteProviderWithClerk.svelte.d.ts.map +1 -0
  66. package/dist/svelte/auth/RegisterClerkAuth.svelte +27 -0
  67. package/dist/svelte/auth/RegisterClerkAuth.svelte.d.ts +17 -0
  68. package/dist/svelte/auth/RegisterClerkAuth.svelte.d.ts.map +1 -0
  69. package/dist/svelte/auth/index.d.ts +2 -0
  70. package/dist/svelte/auth/index.d.ts.map +1 -1
  71. package/dist/svelte/auth/index.js +2 -0
  72. package/dist/svelte/tests/ClerkAuth.svelte.test.d.ts +2 -0
  73. package/dist/svelte/tests/ClerkAuth.svelte.test.d.ts.map +1 -0
  74. package/dist/svelte/tests/ClerkAuth.svelte.test.js +202 -0
  75. package/dist/svelte/tests/TestClerkAuthWrapper.svelte +16 -0
  76. package/dist/svelte/tests/TestClerkAuthWrapper.svelte.d.ts +8 -0
  77. package/dist/svelte/tests/TestClerkAuthWrapper.svelte.d.ts.map +1 -0
  78. package/dist/svelte/tests/testUtils.d.ts +1 -0
  79. package/dist/svelte/tests/testUtils.d.ts.map +1 -1
  80. package/dist/svelte/tests/testUtils.js +3 -1
  81. package/dist/testing.js +1 -1
  82. package/dist/tools/auth/clerk/index.d.ts +1 -1
  83. package/dist/tools/auth/clerk/types.d.ts +1 -1
  84. package/dist/tools/auth/clerk/types.d.ts.map +1 -1
  85. package/dist/tools/coValues/account.d.ts +5 -1
  86. package/dist/tools/coValues/account.d.ts.map +1 -1
  87. package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts +30 -1
  88. package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts.map +1 -1
  89. package/dist/tools/implementation/zodSchema/zodCo.d.ts.map +1 -1
  90. package/dist/tools/subscribe/types.d.ts +1 -1
  91. package/dist/tools/subscribe/types.d.ts.map +1 -1
  92. package/dist/tools/testing.d.ts.map +1 -1
  93. package/package.json +8 -4
  94. package/src/better-auth/auth/client.ts +3 -1
  95. package/src/better-auth/auth/server.ts +9 -7
  96. package/src/better-auth/auth/tests/client.test.ts +66 -2
  97. package/src/better-auth/database-adapter/repository/generic.ts +3 -3
  98. package/src/better-auth/database-adapter/repository/session.ts +2 -2
  99. package/src/better-auth/database-adapter/schema.ts +5 -5
  100. package/src/react/hooks.tsx +4 -2
  101. package/src/react-core/hooks.ts +332 -178
  102. package/src/react-core/tests/useCoState.selector.test.ts +309 -22
  103. package/src/react-core/tests/useCoStates.test.tsx +414 -0
  104. package/src/react-native-core/auth/PasskeyAuth.ts +316 -0
  105. package/src/react-native-core/auth/PasskeyAuthBasicUI.tsx +284 -0
  106. package/src/react-native-core/auth/auth.ts +3 -0
  107. package/src/react-native-core/auth/passkey-utils.ts +47 -0
  108. package/src/react-native-core/auth/usePasskeyAuth.tsx +85 -0
  109. package/src/react-native-core/hooks.tsx +2 -0
  110. package/src/react-native-core/tests/PasskeyAuth.test.ts +463 -0
  111. package/src/react-native-core/tests/passkey-utils.test.ts +144 -0
  112. package/src/svelte/auth/ClerkAuth.svelte.ts +67 -0
  113. package/src/svelte/auth/JazzSvelteProviderWithClerk.svelte +156 -0
  114. package/src/svelte/auth/RegisterClerkAuth.svelte +27 -0
  115. package/src/svelte/auth/index.ts +2 -0
  116. package/src/svelte/tests/ClerkAuth.svelte.test.ts +305 -0
  117. package/src/svelte/tests/TestClerkAuthWrapper.svelte +16 -0
  118. package/src/svelte/tests/testUtils.ts +4 -1
  119. package/src/tools/auth/clerk/types.ts +1 -1
  120. package/src/tools/coValues/account.ts +11 -3
  121. package/src/tools/implementation/zodSchema/schemaTypes/AccountSchema.ts +27 -1
  122. package/src/tools/subscribe/types.ts +1 -1
  123. package/src/tools/tests/account.test.ts +2 -1
  124. package/src/tools/tests/inbox.test.ts +7 -7
  125. package/testSetup.ts +4 -0
  126. package/vitest.config.ts +1 -0
  127. package/dist/chunk-PEHQ7TN2.js.map +0 -1
@@ -0,0 +1,85 @@
1
+ import {
2
+ useAuthSecretStorage,
3
+ useIsAuthenticated,
4
+ useJazzContext,
5
+ } from "jazz-tools/react-core";
6
+ import { useMemo } from "react";
7
+ import { ReactNativePasskeyAuth } from "./PasskeyAuth.js";
8
+
9
+ /**
10
+ * React hook for passkey (WebAuthn) authentication in React Native apps.
11
+ *
12
+ * This hook provides a simple interface for signing up and logging in with passkeys,
13
+ * using the device's biometric authentication (FaceID/TouchID/fingerprint).
14
+ *
15
+ * **Requirements:**
16
+ * - Install `react-native-passkey` as a peer dependency
17
+ * - Configure your app's associated domains (iOS) and asset links (Android)
18
+ * - Passkeys require HTTPS domain verification
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * import { usePasskeyAuth } from "jazz-tools/react-native-core";
23
+ *
24
+ * function AuthScreen() {
25
+ * const auth = usePasskeyAuth({
26
+ * appName: "My App",
27
+ * rpId: "myapp.com",
28
+ * });
29
+ *
30
+ * if (auth.state === "signedIn") {
31
+ * return <MainApp />;
32
+ * }
33
+ *
34
+ * return (
35
+ * <View>
36
+ * <Button title="Sign Up" onPress={() => auth.signUp("John Doe")} />
37
+ * <Button title="Log In" onPress={auth.logIn} />
38
+ * </View>
39
+ * );
40
+ * }
41
+ * ```
42
+ *
43
+ * @param options.appName - The display name of your app shown during passkey prompts
44
+ * @param options.rpId - The relying party ID (your app's domain, e.g., "myapp.com")
45
+ *
46
+ * @category Auth Providers
47
+ */
48
+ export function usePasskeyAuth({
49
+ appName,
50
+ rpId,
51
+ }: {
52
+ appName: string;
53
+ rpId: string;
54
+ }) {
55
+ const context = useJazzContext();
56
+ const authSecretStorage = useAuthSecretStorage();
57
+
58
+ if ("guest" in context) {
59
+ throw new Error("Passkey auth is not supported in guest mode");
60
+ }
61
+
62
+ const authMethod = useMemo(() => {
63
+ return new ReactNativePasskeyAuth(
64
+ context.node.crypto,
65
+ context.authenticate,
66
+ authSecretStorage,
67
+ appName,
68
+ rpId,
69
+ );
70
+ }, [
71
+ appName,
72
+ rpId,
73
+ authSecretStorage,
74
+ context.node.crypto,
75
+ context.authenticate,
76
+ ]);
77
+
78
+ const isAuthenticated = useIsAuthenticated();
79
+
80
+ return {
81
+ state: isAuthenticated ? "signedIn" : "anonymous",
82
+ logIn: authMethod.logIn,
83
+ signUp: authMethod.signUp,
84
+ } as const;
85
+ }
@@ -6,6 +6,7 @@ import { Linking } from "react-native";
6
6
 
7
7
  export {
8
8
  useCoState,
9
+ useCoStates,
9
10
  experimental_useInboxSender,
10
11
  useDemoAuth,
11
12
  usePassphraseAuth,
@@ -20,6 +21,7 @@ export {
20
21
  useAccountSubscription,
21
22
  useSubscriptionSelector,
22
23
  useSuspenseCoState,
24
+ useSuspenseCoStates,
23
25
  useSuspenseAccount,
24
26
  } from "jazz-tools/react-core";
25
27
 
@@ -0,0 +1,463 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { AgentSecret } from "cojson";
4
+ import { Account, InMemoryKVStore, KvStoreContext } from "jazz-tools";
5
+ import { AuthSecretStorage } from "jazz-tools";
6
+ import { createJazzTestAccount } from "jazz-tools/testing";
7
+ import { beforeEach, describe, expect, it, vi } from "vitest";
8
+ import {
9
+ ReactNativePasskeyAuth,
10
+ setPasskeyModule,
11
+ } from "../auth/PasskeyAuth.js";
12
+ import {
13
+ base64UrlToUint8Array,
14
+ uint8ArrayToBase64Url,
15
+ } from "../auth/passkey-utils.js";
16
+
17
+ // Create mock functions
18
+ const mockCreate = vi.fn();
19
+ const mockGet = vi.fn();
20
+ const mockIsSupported = vi.fn();
21
+
22
+ // Create mock passkey module
23
+ const mockPasskeyModule = {
24
+ create: mockCreate,
25
+ get: mockGet,
26
+ isSupported: mockIsSupported,
27
+ };
28
+
29
+ KvStoreContext.getInstance().initialize(new InMemoryKVStore());
30
+ const authSecretStorage = new AuthSecretStorage();
31
+
32
+ beforeEach(async () => {
33
+ await authSecretStorage.clear();
34
+ vi.clearAllMocks();
35
+
36
+ // Inject the mock module using dependency injection
37
+ setPasskeyModule(mockPasskeyModule);
38
+
39
+ await createJazzTestAccount({
40
+ isCurrentActiveAccount: true,
41
+ });
42
+ });
43
+
44
+ describe("ReactNativePasskeyAuth", () => {
45
+ const mockCrypto = {
46
+ randomBytes: (l: number) => crypto.getRandomValues(new Uint8Array(l)),
47
+ newRandomSecretSeed: () => new Uint8Array(32).fill(1),
48
+ agentSecretFromSecretSeed: () => "mock-secret" as AgentSecret,
49
+ } as any;
50
+ const mockAuthenticate = vi.fn();
51
+
52
+ describe("initialization", () => {
53
+ it("should initialize with app name and rpId", () => {
54
+ const auth = new ReactNativePasskeyAuth(
55
+ mockCrypto,
56
+ mockAuthenticate,
57
+ authSecretStorage,
58
+ "Test App",
59
+ "example.com",
60
+ );
61
+ expect(auth.appName).toBe("Test App");
62
+ expect(auth.rpId).toBe("example.com");
63
+ });
64
+
65
+ it("should have static id property", () => {
66
+ expect(ReactNativePasskeyAuth.id).toBe("passkey");
67
+ });
68
+ });
69
+
70
+ describe("logIn", () => {
71
+ it("should call Passkey.get with correct parameters", async () => {
72
+ const auth = new ReactNativePasskeyAuth(
73
+ mockCrypto,
74
+ mockAuthenticate,
75
+ authSecretStorage,
76
+ "Test App",
77
+ "example.com",
78
+ );
79
+
80
+ // Create a mock credential payload (secretSeed + accountID)
81
+ const mockPayload = new Uint8Array(56);
82
+ mockPayload.fill(1, 0, 32); // secretSeed
83
+ mockPayload.fill(2, 32, 56); // accountID hash
84
+
85
+ mockGet.mockResolvedValue({
86
+ id: "credential-id",
87
+ rawId: "raw-credential-id",
88
+ type: "public-key",
89
+ response: {
90
+ clientDataJSON: "mock-client-data",
91
+ authenticatorData: "mock-auth-data",
92
+ signature: "mock-signature",
93
+ userHandle: uint8ArrayToBase64Url(mockPayload),
94
+ },
95
+ });
96
+
97
+ await auth.logIn();
98
+
99
+ expect(mockGet).toHaveBeenCalledWith({
100
+ challenge: expect.any(String),
101
+ rpId: "example.com",
102
+ timeout: 60000,
103
+ userVerification: "preferred",
104
+ });
105
+
106
+ expect(mockAuthenticate).toHaveBeenCalledWith({
107
+ accountID: expect.any(String),
108
+ accountSecret: "mock-secret",
109
+ });
110
+ });
111
+
112
+ it("should store credentials after successful login", async () => {
113
+ const auth = new ReactNativePasskeyAuth(
114
+ mockCrypto,
115
+ mockAuthenticate,
116
+ authSecretStorage,
117
+ "Test App",
118
+ "example.com",
119
+ );
120
+
121
+ const mockPayload = new Uint8Array(56);
122
+ mockPayload.fill(1, 0, 32);
123
+ mockPayload.fill(2, 32, 56);
124
+
125
+ mockGet.mockResolvedValue({
126
+ id: "credential-id",
127
+ rawId: "raw-credential-id",
128
+ type: "public-key",
129
+ response: {
130
+ clientDataJSON: "mock-client-data",
131
+ authenticatorData: "mock-auth-data",
132
+ signature: "mock-signature",
133
+ userHandle: uint8ArrayToBase64Url(mockPayload),
134
+ },
135
+ });
136
+
137
+ await auth.logIn();
138
+
139
+ const stored = await authSecretStorage.get();
140
+ expect(stored).toEqual({
141
+ accountID: expect.any(String),
142
+ secretSeed: expect.any(Uint8Array),
143
+ accountSecret: "mock-secret",
144
+ provider: "passkey",
145
+ });
146
+ });
147
+
148
+ it("should throw error when passkey authentication fails", async () => {
149
+ const auth = new ReactNativePasskeyAuth(
150
+ mockCrypto,
151
+ mockAuthenticate,
152
+ authSecretStorage,
153
+ "Test App",
154
+ "example.com",
155
+ );
156
+
157
+ mockGet.mockRejectedValue(new Error("User cancelled"));
158
+
159
+ await expect(auth.logIn()).rejects.toThrow(
160
+ "Passkey authentication aborted",
161
+ );
162
+ });
163
+
164
+ it("should return early when passkey.get returns null", async () => {
165
+ const auth = new ReactNativePasskeyAuth(
166
+ mockCrypto,
167
+ mockAuthenticate,
168
+ authSecretStorage,
169
+ "Test App",
170
+ "example.com",
171
+ );
172
+
173
+ mockGet.mockResolvedValue(null);
174
+
175
+ await auth.logIn();
176
+
177
+ expect(mockAuthenticate).not.toHaveBeenCalled();
178
+ });
179
+
180
+ it("should throw error when userHandle is null", async () => {
181
+ const auth = new ReactNativePasskeyAuth(
182
+ mockCrypto,
183
+ mockAuthenticate,
184
+ authSecretStorage,
185
+ "Test App",
186
+ "example.com",
187
+ );
188
+
189
+ mockGet.mockResolvedValue({
190
+ id: "credential-id",
191
+ rawId: "raw-credential-id",
192
+ type: "public-key",
193
+ response: {
194
+ clientDataJSON: "mock-client-data",
195
+ authenticatorData: "mock-auth-data",
196
+ signature: "mock-signature",
197
+ userHandle: null,
198
+ },
199
+ });
200
+
201
+ await expect(auth.logIn()).rejects.toThrow(
202
+ "Passkey credential is missing userHandle",
203
+ );
204
+ });
205
+ });
206
+
207
+ describe("signUp", () => {
208
+ it("should call Passkey.create with correct parameters", async () => {
209
+ // Use the real account from createJazzTestAccount
210
+ const me = await Account.getMe().$jazz.ensureLoaded({ resolve: true });
211
+
212
+ const auth = new ReactNativePasskeyAuth(
213
+ mockCrypto,
214
+ mockAuthenticate,
215
+ authSecretStorage,
216
+ "Test App",
217
+ "example.com",
218
+ );
219
+
220
+ // Set up credentials with the real account ID
221
+ await authSecretStorage.set({
222
+ accountID: me.$jazz.id,
223
+ secretSeed: new Uint8Array(32).fill(1),
224
+ accountSecret: "mock-secret" as AgentSecret,
225
+ provider: "anonymous",
226
+ });
227
+
228
+ mockCreate.mockResolvedValue({
229
+ id: "credential-id",
230
+ rawId: "raw-credential-id",
231
+ type: "public-key",
232
+ response: {
233
+ clientDataJSON: "mock-client-data",
234
+ attestationObject: "mock-attestation",
235
+ },
236
+ });
237
+
238
+ await auth.signUp("testuser");
239
+
240
+ expect(mockCreate).toHaveBeenCalledWith({
241
+ challenge: expect.any(String),
242
+ rp: {
243
+ id: "example.com",
244
+ name: "Test App",
245
+ },
246
+ user: {
247
+ id: expect.any(String),
248
+ name: expect.stringContaining("testuser"),
249
+ displayName: "testuser",
250
+ },
251
+ pubKeyCredParams: [
252
+ { alg: -7, type: "public-key" },
253
+ { alg: -257, type: "public-key" },
254
+ ],
255
+ authenticatorSelection: {
256
+ residentKey: "required",
257
+ userVerification: "preferred",
258
+ },
259
+ timeout: 60000,
260
+ attestation: "none",
261
+ });
262
+ });
263
+
264
+ it("should update provider to passkey after signup", async () => {
265
+ const me = await Account.getMe().$jazz.ensureLoaded({ resolve: true });
266
+
267
+ const auth = new ReactNativePasskeyAuth(
268
+ mockCrypto,
269
+ mockAuthenticate,
270
+ authSecretStorage,
271
+ "Test App",
272
+ "example.com",
273
+ );
274
+
275
+ await authSecretStorage.set({
276
+ accountID: me.$jazz.id,
277
+ secretSeed: new Uint8Array(32).fill(1),
278
+ accountSecret: "mock-secret" as AgentSecret,
279
+ provider: "anonymous",
280
+ });
281
+
282
+ mockCreate.mockResolvedValue({
283
+ id: "credential-id",
284
+ rawId: "raw-credential-id",
285
+ type: "public-key",
286
+ response: {
287
+ clientDataJSON: "mock-client-data",
288
+ attestationObject: "mock-attestation",
289
+ },
290
+ });
291
+
292
+ await auth.signUp("testuser");
293
+
294
+ const stored = await authSecretStorage.get();
295
+ expect(stored?.provider).toBe("passkey");
296
+ });
297
+
298
+ it("should throw error when no credentials exist", async () => {
299
+ await authSecretStorage.clear();
300
+
301
+ const auth = new ReactNativePasskeyAuth(
302
+ mockCrypto,
303
+ mockAuthenticate,
304
+ authSecretStorage,
305
+ "Test App",
306
+ "example.com",
307
+ );
308
+
309
+ await expect(auth.signUp("testuser")).rejects.toThrow(
310
+ "Not enough credentials to register the account with passkey",
311
+ );
312
+ });
313
+
314
+ it("should throw error when passkey creation fails", async () => {
315
+ const me = await Account.getMe().$jazz.ensureLoaded({ resolve: true });
316
+
317
+ const auth = new ReactNativePasskeyAuth(
318
+ mockCrypto,
319
+ mockAuthenticate,
320
+ authSecretStorage,
321
+ "Test App",
322
+ "example.com",
323
+ );
324
+
325
+ await authSecretStorage.set({
326
+ accountID: me.$jazz.id,
327
+ secretSeed: new Uint8Array(32).fill(1),
328
+ accountSecret: "mock-secret" as AgentSecret,
329
+ provider: "anonymous",
330
+ });
331
+
332
+ mockCreate.mockRejectedValue(new Error("User cancelled"));
333
+
334
+ await expect(auth.signUp("testuser")).rejects.toThrow(
335
+ "Passkey creation aborted",
336
+ );
337
+ });
338
+
339
+ it("should leave profile name unchanged if username is empty", async () => {
340
+ const me = await Account.getMe().$jazz.ensureLoaded({ resolve: true });
341
+
342
+ const auth = new ReactNativePasskeyAuth(
343
+ mockCrypto,
344
+ mockAuthenticate,
345
+ authSecretStorage,
346
+ "Test App",
347
+ "example.com",
348
+ );
349
+
350
+ await authSecretStorage.set({
351
+ accountID: me.$jazz.id,
352
+ secretSeed: new Uint8Array(32).fill(1),
353
+ accountSecret: "mock-secret" as AgentSecret,
354
+ provider: "anonymous",
355
+ });
356
+
357
+ mockCreate.mockResolvedValue({
358
+ id: "credential-id",
359
+ rawId: "raw-credential-id",
360
+ type: "public-key",
361
+ response: {
362
+ clientDataJSON: "mock-client-data",
363
+ attestationObject: "mock-attestation",
364
+ },
365
+ });
366
+
367
+ await auth.signUp("");
368
+
369
+ const currentAccount = await Account.getMe().$jazz.ensureLoaded({
370
+ resolve: {
371
+ profile: true,
372
+ },
373
+ });
374
+
375
+ // 'Test Account' is the name provided during account creation
376
+ expect(currentAccount.profile.name).toEqual("Test Account");
377
+ });
378
+
379
+ it("should update profile name if username is provided", async () => {
380
+ const me = await Account.getMe().$jazz.ensureLoaded({ resolve: true });
381
+
382
+ const auth = new ReactNativePasskeyAuth(
383
+ mockCrypto,
384
+ mockAuthenticate,
385
+ authSecretStorage,
386
+ "Test App",
387
+ "example.com",
388
+ );
389
+
390
+ await authSecretStorage.set({
391
+ accountID: me.$jazz.id,
392
+ secretSeed: new Uint8Array(32).fill(1),
393
+ accountSecret: "mock-secret" as AgentSecret,
394
+ provider: "anonymous",
395
+ });
396
+
397
+ mockCreate.mockResolvedValue({
398
+ id: "credential-id",
399
+ rawId: "raw-credential-id",
400
+ type: "public-key",
401
+ response: {
402
+ clientDataJSON: "mock-client-data",
403
+ attestationObject: "mock-attestation",
404
+ },
405
+ });
406
+
407
+ await auth.signUp("testuser");
408
+
409
+ const currentAccount = await Account.getMe().$jazz.ensureLoaded({
410
+ resolve: {
411
+ profile: true,
412
+ },
413
+ });
414
+
415
+ expect(currentAccount.profile.name).toEqual("testuser");
416
+ });
417
+ });
418
+
419
+ describe("credential encoding", () => {
420
+ it("should encode user.id as base64url in create request", async () => {
421
+ const me = await Account.getMe().$jazz.ensureLoaded({ resolve: true });
422
+
423
+ const auth = new ReactNativePasskeyAuth(
424
+ mockCrypto,
425
+ mockAuthenticate,
426
+ authSecretStorage,
427
+ "Test App",
428
+ "example.com",
429
+ );
430
+
431
+ await authSecretStorage.set({
432
+ accountID: me.$jazz.id,
433
+ secretSeed: new Uint8Array(32).fill(1),
434
+ accountSecret: "mock-secret" as AgentSecret,
435
+ provider: "anonymous",
436
+ });
437
+
438
+ mockCreate.mockResolvedValue({
439
+ id: "credential-id",
440
+ rawId: "raw-credential-id",
441
+ type: "public-key",
442
+ response: {
443
+ clientDataJSON: "mock-client-data",
444
+ attestationObject: "mock-attestation",
445
+ },
446
+ });
447
+
448
+ await auth.signUp("testuser");
449
+
450
+ const createCall = mockCreate.mock.calls[0]![0];
451
+ const userId = createCall.user.id;
452
+
453
+ // Should be a valid base64url string (no +, /, or =)
454
+ expect(userId).not.toContain("+");
455
+ expect(userId).not.toContain("/");
456
+ expect(userId).not.toContain("=");
457
+
458
+ // Should decode to expected length (secretSeedLength 32 + shortHashLength 19 = 51 bytes)
459
+ const decoded = base64UrlToUint8Array(userId);
460
+ expect(decoded.length).toBe(51);
461
+ });
462
+ });
463
+ });
@@ -0,0 +1,144 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ base64UrlToUint8Array,
4
+ uint8ArrayToBase64Url,
5
+ } from "../auth/passkey-utils";
6
+
7
+ describe("passkey-utils", () => {
8
+ describe("uint8ArrayToBase64Url", () => {
9
+ it("should encode an empty array", () => {
10
+ const bytes = new Uint8Array([]);
11
+ expect(uint8ArrayToBase64Url(bytes)).toBe("");
12
+ });
13
+
14
+ it("should encode a simple byte array", () => {
15
+ // "Hello" in bytes
16
+ const bytes = new Uint8Array([72, 101, 108, 108, 111]);
17
+ expect(uint8ArrayToBase64Url(bytes)).toBe("SGVsbG8");
18
+ });
19
+
20
+ it("should use base64url alphabet (- instead of +)", () => {
21
+ // Bytes that produce + in standard base64
22
+ const bytes = new Uint8Array([251, 239]); // produces "++" in base64
23
+ const result = uint8ArrayToBase64Url(bytes);
24
+ expect(result).not.toContain("+");
25
+ expect(result).toContain("-");
26
+ });
27
+
28
+ it("should use base64url alphabet (_ instead of /)", () => {
29
+ // Bytes that produce / in standard base64
30
+ const bytes = new Uint8Array([255, 255]); // produces "//" in base64
31
+ const result = uint8ArrayToBase64Url(bytes);
32
+ expect(result).not.toContain("/");
33
+ expect(result).toContain("_");
34
+ });
35
+
36
+ it("should omit padding", () => {
37
+ // Single byte produces base64 with padding
38
+ const bytes = new Uint8Array([1]);
39
+ const result = uint8ArrayToBase64Url(bytes);
40
+ expect(result).not.toContain("=");
41
+ });
42
+
43
+ it("should handle typical credential payload size (56 bytes)", () => {
44
+ // secretSeedLength (32) + shortHashLength (19) = 51 bytes
45
+ const bytes = new Uint8Array(56);
46
+ for (let i = 0; i < 56; i++) {
47
+ bytes[i] = i;
48
+ }
49
+ const result = uint8ArrayToBase64Url(bytes);
50
+ expect(result.length).toBeGreaterThan(0);
51
+ expect(result).not.toContain("+");
52
+ expect(result).not.toContain("/");
53
+ expect(result).not.toContain("=");
54
+ });
55
+ });
56
+
57
+ describe("base64UrlToUint8Array", () => {
58
+ it("should decode an empty string", () => {
59
+ const result = base64UrlToUint8Array("");
60
+ expect(result).toEqual(new Uint8Array([]));
61
+ });
62
+
63
+ it("should decode a simple base64url string", () => {
64
+ // "SGVsbG8" is "Hello" in base64url
65
+ const result = base64UrlToUint8Array("SGVsbG8");
66
+ expect(result).toEqual(new Uint8Array([72, 101, 108, 108, 111]));
67
+ });
68
+
69
+ it("should handle - character (base64url for +)", () => {
70
+ const result = base64UrlToUint8Array("--8");
71
+ expect(result).toBeInstanceOf(Uint8Array);
72
+ });
73
+
74
+ it("should handle _ character (base64url for /)", () => {
75
+ const result = base64UrlToUint8Array("__8");
76
+ expect(result).toBeInstanceOf(Uint8Array);
77
+ });
78
+
79
+ it("should add padding automatically", () => {
80
+ // "AQ" needs padding to become "AQ==" for valid base64
81
+ const result = base64UrlToUint8Array("AQ");
82
+ expect(result).toEqual(new Uint8Array([1]));
83
+ });
84
+
85
+ it("should handle strings that need 1 padding char", () => {
86
+ // 3 chars needs 1 padding
87
+ const result = base64UrlToUint8Array("ABC");
88
+ expect(result).toBeInstanceOf(Uint8Array);
89
+ expect(result.length).toBe(2);
90
+ });
91
+
92
+ it("should handle strings that need 2 padding chars", () => {
93
+ // 2 chars needs 2 padding
94
+ const result = base64UrlToUint8Array("AB");
95
+ expect(result).toBeInstanceOf(Uint8Array);
96
+ expect(result.length).toBe(1);
97
+ });
98
+ });
99
+
100
+ describe("roundtrip encoding/decoding", () => {
101
+ it("should roundtrip empty array", () => {
102
+ const original = new Uint8Array([]);
103
+ const encoded = uint8ArrayToBase64Url(original);
104
+ const decoded = base64UrlToUint8Array(encoded);
105
+ expect(decoded).toEqual(original);
106
+ });
107
+
108
+ it("should roundtrip simple bytes", () => {
109
+ const original = new Uint8Array([1, 2, 3, 4, 5]);
110
+ const encoded = uint8ArrayToBase64Url(original);
111
+ const decoded = base64UrlToUint8Array(encoded);
112
+ expect(decoded).toEqual(original);
113
+ });
114
+
115
+ it("should roundtrip all byte values", () => {
116
+ const original = new Uint8Array(256);
117
+ for (let i = 0; i < 256; i++) {
118
+ original[i] = i;
119
+ }
120
+ const encoded = uint8ArrayToBase64Url(original);
121
+ const decoded = base64UrlToUint8Array(encoded);
122
+ expect(decoded).toEqual(original);
123
+ });
124
+
125
+ it("should roundtrip credential-sized payload", () => {
126
+ // Typical passkey credential payload: secretSeed + accountID hash
127
+ const original = new Uint8Array(56);
128
+ crypto.getRandomValues(original);
129
+ const encoded = uint8ArrayToBase64Url(original);
130
+ const decoded = base64UrlToUint8Array(encoded);
131
+ expect(decoded).toEqual(original);
132
+ });
133
+
134
+ it("should roundtrip random data of various sizes", () => {
135
+ for (const size of [1, 2, 3, 10, 32, 64, 100, 256]) {
136
+ const original = new Uint8Array(size);
137
+ crypto.getRandomValues(original);
138
+ const encoded = uint8ArrayToBase64Url(original);
139
+ const decoded = base64UrlToUint8Array(encoded);
140
+ expect(decoded).toEqual(original);
141
+ }
142
+ });
143
+ });
144
+ });