jazz-tools 0.19.19 → 0.19.20

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 (64) 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__/tests/client.test.js +48 -0
  4. package/.turbo/turbo-build.log +65 -61
  5. package/dist/better-auth/auth/client.d.ts.map +1 -1
  6. package/dist/better-auth/auth/client.js +1 -1
  7. package/dist/better-auth/auth/client.js.map +1 -1
  8. package/dist/{chunk-PEHQ7TN2.js → chunk-MI24YFCY.js} +31 -4
  9. package/dist/chunk-MI24YFCY.js.map +1 -0
  10. package/dist/index.js +1 -1
  11. package/dist/react-core/hooks.d.ts +2 -2
  12. package/dist/react-core/hooks.d.ts.map +1 -1
  13. package/dist/react-core/index.js +4 -78
  14. package/dist/react-core/index.js.map +1 -1
  15. package/dist/react-native/chunk-DGUM43GV.js +11 -0
  16. package/dist/react-native/chunk-DGUM43GV.js.map +1 -0
  17. package/dist/react-native/crypto.js +2 -0
  18. package/dist/react-native/crypto.js.map +1 -1
  19. package/dist/react-native/index.js +540 -29
  20. package/dist/react-native/index.js.map +1 -1
  21. package/dist/react-native-core/auth/PasskeyAuth.d.ts +123 -0
  22. package/dist/react-native-core/auth/PasskeyAuth.d.ts.map +1 -0
  23. package/dist/react-native-core/auth/PasskeyAuthBasicUI.d.ts +34 -0
  24. package/dist/react-native-core/auth/PasskeyAuthBasicUI.d.ts.map +1 -0
  25. package/dist/react-native-core/auth/auth.d.ts +3 -0
  26. package/dist/react-native-core/auth/auth.d.ts.map +1 -1
  27. package/dist/react-native-core/auth/passkey-utils.d.ts +16 -0
  28. package/dist/react-native-core/auth/passkey-utils.d.ts.map +1 -0
  29. package/dist/react-native-core/auth/usePasskeyAuth.d.ts +48 -0
  30. package/dist/react-native-core/auth/usePasskeyAuth.d.ts.map +1 -0
  31. package/dist/react-native-core/chunk-DGUM43GV.js +11 -0
  32. package/dist/react-native-core/chunk-DGUM43GV.js.map +1 -0
  33. package/dist/react-native-core/crypto.js +2 -0
  34. package/dist/react-native-core/crypto.js.map +1 -1
  35. package/dist/react-native-core/index.js +535 -24
  36. package/dist/react-native-core/index.js.map +1 -1
  37. package/dist/react-native-core/tests/PasskeyAuth.test.d.ts +2 -0
  38. package/dist/react-native-core/tests/PasskeyAuth.test.d.ts.map +1 -0
  39. package/dist/react-native-core/tests/passkey-utils.test.d.ts +2 -0
  40. package/dist/react-native-core/tests/passkey-utils.test.d.ts.map +1 -0
  41. package/dist/testing.js +1 -1
  42. package/dist/tools/coValues/account.d.ts +5 -1
  43. package/dist/tools/coValues/account.d.ts.map +1 -1
  44. package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts +30 -1
  45. package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts.map +1 -1
  46. package/dist/tools/implementation/zodSchema/zodCo.d.ts.map +1 -1
  47. package/dist/tools/testing.d.ts.map +1 -1
  48. package/package.json +8 -4
  49. package/src/better-auth/auth/client.ts +3 -1
  50. package/src/better-auth/auth/tests/client.test.ts +66 -2
  51. package/src/react-core/hooks.ts +12 -103
  52. package/src/react-native-core/auth/PasskeyAuth.ts +316 -0
  53. package/src/react-native-core/auth/PasskeyAuthBasicUI.tsx +284 -0
  54. package/src/react-native-core/auth/auth.ts +3 -0
  55. package/src/react-native-core/auth/passkey-utils.ts +47 -0
  56. package/src/react-native-core/auth/usePasskeyAuth.tsx +85 -0
  57. package/src/react-native-core/tests/PasskeyAuth.test.ts +463 -0
  58. package/src/react-native-core/tests/passkey-utils.test.ts +144 -0
  59. package/src/tools/coValues/account.ts +11 -3
  60. package/src/tools/implementation/zodSchema/schemaTypes/AccountSchema.ts +27 -1
  61. package/src/tools/tests/account.test.ts +2 -1
  62. package/testSetup.ts +4 -0
  63. package/vitest.config.ts +1 -0
  64. package/dist/chunk-PEHQ7TN2.js.map +0 -1
@@ -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
+ });
@@ -271,7 +271,12 @@ export class Account extends CoValueBase implements CoValue {
271
271
  worker: Account,
272
272
  options: {
273
273
  creationProps: { name: string };
274
- onCreate?: (account: A, worker: Account) => Promise<void>;
274
+ onCreate?: (
275
+ account: A,
276
+ worker: Account,
277
+ credentials: { accountID: string; accountSecret: AgentSecret },
278
+ ) => Promise<void>;
279
+ waitForSyncTimeout?: number;
275
280
  },
276
281
  ): Promise<{
277
282
  credentials: {
@@ -311,9 +316,12 @@ export class Account extends CoValueBase implements CoValue {
311
316
  throw new Error("Unable to load the worker account");
312
317
 
313
318
  // The onCreate hook can be helpful to define inline logic, such as querying the DB
314
- if (options.onCreate) await options.onCreate(account, loadedWorker);
319
+ if (options.onCreate)
320
+ await options.onCreate(account, loadedWorker, credentials);
315
321
 
316
- await account.$jazz.waitForAllCoValuesSync();
322
+ await account.$jazz.waitForAllCoValuesSync({
323
+ timeout: options.waitForSyncTimeout,
324
+ });
317
325
 
318
326
  const createdAccount = await this.load(account.$jazz.id, {
319
327
  loadAs: worker,
@@ -94,7 +94,31 @@ export class AccountSchema<
94
94
  );
95
95
  }
96
96
 
97
- // Create an account via worker, useful to generate controlled accounts from the server
97
+ /**
98
+ * Creates a new account as a worker account, useful for generating controlled accounts from a server environment.
99
+ * This method initializes a new account, applies migrations, invokes the `onCreate` callback, and then shuts down the temporary node to avoid memory leaks.
100
+ * Returns the created account (loaded on the worker) and its credentials.
101
+ *
102
+ * The method internally calls `waitForAllCoValuesSync` on the new account. If many CoValues are created during `onCreate`,
103
+ * consider adjusting the timeout using the `waitForSyncTimeout` option.
104
+ *
105
+ * @param worker - The worker account to create the new account from
106
+ * @param options.creationProps - The creation properties for the new account
107
+ * @param options.onCreate - The callback to use to initialize the account after it is created
108
+ * @param options.waitForSyncTimeout - The timeout for the sync to complete
109
+ * @returns The credentials and the created account loaded by the worker account
110
+ *
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * const { credentials, account } = await AccountSchema.createAs(worker, {
115
+ * creationProps: { name: "My Account" },
116
+ * onCreate: async (account, worker, credentials) => {
117
+ * account.root.$jazz.owner.addMember(worker, "writer");
118
+ * },
119
+ * });
120
+ * ```
121
+ */
98
122
  createAs(
99
123
  worker: Account,
100
124
  options: {
@@ -102,7 +126,9 @@ export class AccountSchema<
102
126
  onCreate?: (
103
127
  account: AccountInstance<Shape>,
104
128
  worker: Account,
129
+ credentials: { accountID: string; accountSecret: AgentSecret },
105
130
  ) => Promise<void>;
131
+ waitForSyncTimeout?: number;
106
132
  },
107
133
  ): Promise<{
108
134
  credentials: {
@@ -469,8 +469,9 @@ describe("createAs", () => {
469
469
 
470
470
  const created = await CustomAccount.createAs(worker, {
471
471
  creationProps: { name: "Test Account" },
472
- onCreate: async (account, loadedWorker) => {
472
+ onCreate: async (account, loadedWorker, credentials) => {
473
473
  executionOrder.push("onCreate");
474
+ expect(credentials.accountID).toBe(account.$jazz.id);
474
475
 
475
476
  // Verify migration ran before onCreate
476
477
  expect(executionOrder).toEqual(["migration", "onCreate"]);
package/testSetup.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { cojsonInternals } from "cojson";
2
+
3
+ // Use a very high budget to avoid that slow tests fail due to the budget being exceeded.
4
+ cojsonInternals.setIncomingMessagesTimeBudget(10000); // 10 seconds
package/vitest.config.ts CHANGED
@@ -30,6 +30,7 @@ export default defineProject({
30
30
  test: {
31
31
  name: "jazz-tools",
32
32
  include: ["src/**/*.test.{js,ts,tsx,svelte}"],
33
+ setupFiles: ["./testSetup.ts"],
33
34
  typecheck: {
34
35
  enabled: true,
35
36
  checker: "tsc",