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
@@ -417,31 +417,8 @@ export function useCoState<
417
417
  },
418
418
  ): TSelectorReturn {
419
419
  useImportCoValueContent(id, options?.preloaded);
420
-
421
420
  const subscription = useCoValueSubscription(Schema, id, options);
422
- const getCurrentValue = useGetCurrentValue(subscription);
423
-
424
- const value = useSyncExternalStoreWithSelector<
425
- MaybeLoaded<Loaded<S, R>>,
426
- TSelectorReturn
427
- >(
428
- React.useCallback(
429
- (callback) => {
430
- if (!subscription) {
431
- return () => {};
432
- }
433
-
434
- return subscription.subscribe(callback);
435
- },
436
- [subscription],
437
- ),
438
- getCurrentValue,
439
- getCurrentValue,
440
- options?.select ?? ((value) => value as TSelectorReturn),
441
- options?.equalityFn ?? Object.is,
442
- );
443
-
444
- return value;
421
+ return useSubscriptionSelector(subscription, options);
445
422
  }
446
423
 
447
424
  export function useSuspenseCoState<
@@ -491,50 +468,27 @@ export function useSuspenseCoState<
491
468
 
492
469
  use(subscription.getCachedPromise());
493
470
 
494
- const getCurrentValue = () => {
495
- const value = subscription.getCurrentValue();
496
-
497
- if (!value.$isLoaded) {
498
- throw new Error("CoValue must be loaded in a suspense context");
499
- }
500
-
501
- return value;
502
- };
503
-
504
- const value = useSyncExternalStoreWithSelector<Loaded<S, R>, TSelectorReturn>(
505
- React.useCallback(
506
- (callback) => {
507
- return subscription.subscribe(callback);
508
- },
509
- [subscription],
510
- ),
511
- getCurrentValue,
512
- getCurrentValue,
513
- options?.select ?? ((value) => value as TSelectorReturn),
514
- options?.equalityFn ?? Object.is,
515
- );
516
-
517
- return value;
471
+ return useSubscriptionSelector(subscription, options);
518
472
  }
519
473
 
520
474
  export function useSubscriptionSelector<
521
475
  S extends CoValueClassOrSchema,
522
476
  // @ts-expect-error we can't statically enforce the schema's resolve query is a valid resolve query, but in practice it is
523
477
  const R extends ResolveQuery<S> = SchemaResolveQuery<S>,
524
- TSelectorReturn = MaybeLoaded<Loaded<S, R>>,
478
+ // Selector input can be an already loaded or a maybe-loaded value,
479
+ // depending on whether a suspense hook is used or not, respectively.
480
+ TSelectorInput = MaybeLoaded<Loaded<S, R>>,
481
+ TSelectorReturn = TSelectorInput,
525
482
  >(
526
483
  subscription: CoValueSubscription<S, R>,
527
484
  options?: {
528
- select?: (value: MaybeLoaded<Loaded<S, R>>) => TSelectorReturn;
485
+ select?: (value: TSelectorInput) => TSelectorReturn;
529
486
  equalityFn?: (a: TSelectorReturn, b: TSelectorReturn) => boolean;
530
487
  },
531
- ) {
488
+ ): TSelectorReturn {
532
489
  const getCurrentValue = useGetCurrentValue(subscription);
533
490
 
534
- return useSyncExternalStoreWithSelector<
535
- MaybeLoaded<Loaded<S, R>>,
536
- TSelectorReturn
537
- >(
491
+ return useSyncExternalStoreWithSelector<TSelectorInput, TSelectorReturn>(
538
492
  React.useCallback(
539
493
  (callback) => {
540
494
  if (!subscription) {
@@ -547,7 +501,7 @@ export function useSubscriptionSelector<
547
501
  ),
548
502
  getCurrentValue,
549
503
  getCurrentValue,
550
- options?.select ?? ((value) => value as TSelectorReturn),
504
+ options?.select ?? ((value) => value as unknown as TSelectorReturn),
551
505
  options?.equalityFn ?? Object.is,
552
506
  );
553
507
  }
@@ -759,27 +713,7 @@ export function useAccount<
759
713
  },
760
714
  ): TSelectorReturn {
761
715
  const subscription = useAccountSubscription(AccountSchema, options);
762
- const getCurrentValue = useGetCurrentValue(subscription);
763
-
764
- return useSyncExternalStoreWithSelector<
765
- MaybeLoaded<Loaded<A, R>>,
766
- TSelectorReturn
767
- >(
768
- React.useCallback(
769
- (callback) => {
770
- if (!subscription) {
771
- return () => {};
772
- }
773
-
774
- return subscription.subscribe(callback);
775
- },
776
- [subscription],
777
- ),
778
- getCurrentValue,
779
- getCurrentValue,
780
- options?.select ?? ((value) => value as TSelectorReturn),
781
- options?.equalityFn ?? Object.is,
782
- );
716
+ return useSubscriptionSelector(subscription, options);
783
717
  }
784
718
 
785
719
  export function useSuspenseAccount<
@@ -826,32 +760,7 @@ export function useSuspenseAccount<
826
760
 
827
761
  use(subscription.getCachedPromise());
828
762
 
829
- const getCurrentValue = () => {
830
- const value = subscription.getCurrentValue();
831
-
832
- if (!value.$isLoaded) {
833
- throw new Error("Account must be loaded in a suspense context");
834
- }
835
-
836
- return value;
837
- };
838
-
839
- return useSyncExternalStoreWithSelector<Loaded<A, R>, TSelectorReturn>(
840
- React.useCallback(
841
- (callback) => {
842
- if (!subscription) {
843
- return () => {};
844
- }
845
-
846
- return subscription.subscribe(callback);
847
- },
848
- [subscription],
849
- ),
850
- getCurrentValue,
851
- getCurrentValue,
852
- options?.select ?? ((value) => value as TSelectorReturn),
853
- options?.equalityFn ?? Object.is,
854
- );
763
+ return useSubscriptionSelector(subscription, options);
855
764
  }
856
765
 
857
766
  /**
@@ -0,0 +1,316 @@
1
+ import { CryptoProvider, RawAccountID, cojsonInternals } from "cojson";
2
+ import {
3
+ Account,
4
+ AuthSecretStorage,
5
+ AuthenticateAccountFunction,
6
+ ID,
7
+ } from "jazz-tools";
8
+ import {
9
+ base64UrlToUint8Array,
10
+ uint8ArrayToBase64Url,
11
+ } from "./passkey-utils.js";
12
+
13
+ // Types for react-native-passkey library
14
+ // We define these here to avoid requiring the library as a direct dependency
15
+ interface PasskeyCreateRequest {
16
+ challenge: string;
17
+ rp: {
18
+ id: string;
19
+ name: string;
20
+ };
21
+ user: {
22
+ id: string;
23
+ name: string;
24
+ displayName: string;
25
+ };
26
+ pubKeyCredParams: Array<{ alg: number; type: "public-key" }>;
27
+ authenticatorSelection?: {
28
+ authenticatorAttachment?: "platform" | "cross-platform";
29
+ requireResidentKey?: boolean;
30
+ residentKey?: "discouraged" | "preferred" | "required";
31
+ userVerification?: "discouraged" | "preferred" | "required";
32
+ };
33
+ timeout?: number;
34
+ attestation?: "none" | "indirect" | "direct" | "enterprise";
35
+ }
36
+
37
+ interface PasskeyGetRequest {
38
+ challenge: string;
39
+ rpId: string;
40
+ allowCredentials?: Array<{
41
+ id: string;
42
+ type: "public-key";
43
+ transports?: Array<"usb" | "nfc" | "ble" | "internal" | "hybrid">;
44
+ }>;
45
+ timeout?: number;
46
+ userVerification?: "discouraged" | "preferred" | "required";
47
+ }
48
+
49
+ interface PasskeyGetResult {
50
+ id: string;
51
+ rawId: string;
52
+ type: "public-key";
53
+ response: {
54
+ clientDataJSON: string;
55
+ authenticatorData: string;
56
+ signature: string;
57
+ userHandle: string | null;
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Interface for the react-native-passkey module.
63
+ * @internal
64
+ */
65
+ export interface PasskeyModule {
66
+ create: (request: PasskeyCreateRequest) => Promise<unknown>;
67
+ get: (request: PasskeyGetRequest) => Promise<PasskeyGetResult | null>;
68
+ isSupported: () => Promise<boolean>;
69
+ }
70
+
71
+ let cachedPasskeyModule: PasskeyModule | null = null;
72
+
73
+ /**
74
+ * Lazily loads the react-native-passkey module.
75
+ * This allows the module to be an optional peer dependency.
76
+ * @internal
77
+ */
78
+ export function getPasskeyModule(): PasskeyModule {
79
+ if (cachedPasskeyModule) {
80
+ return cachedPasskeyModule;
81
+ }
82
+
83
+ try {
84
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
85
+ const module = require("react-native-passkey");
86
+ const passkeyModule: PasskeyModule =
87
+ module.Passkey || module.default || module;
88
+ cachedPasskeyModule = passkeyModule;
89
+ return passkeyModule;
90
+ } catch (e) {
91
+ console.error("Failed to load react-native-passkey:", e);
92
+ throw new Error(
93
+ "react-native-passkey is not installed. Please install it to use passkey authentication: npm install react-native-passkey",
94
+ );
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Sets a custom passkey module (for testing purposes).
100
+ * @internal
101
+ */
102
+ export function setPasskeyModule(module: PasskeyModule | null): void {
103
+ cachedPasskeyModule = module;
104
+ }
105
+
106
+ /**
107
+ * Check if passkeys are supported on the current device.
108
+ * Returns false if the react-native-passkey module is not available or if the device doesn't support passkeys.
109
+ */
110
+ export async function isPasskeySupported(): Promise<boolean> {
111
+ try {
112
+ const module = getPasskeyModule();
113
+ return await module.isSupported();
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * `ReactNativePasskeyAuth` provides passkey (WebAuthn) authentication for React Native apps.
121
+ *
122
+ * This class uses the device's biometric authentication (FaceID/TouchID/fingerprint) to
123
+ * securely store and retrieve Jazz account credentials.
124
+ *
125
+ * **Requirements:**
126
+ * - Install `react-native-passkey` as a peer dependency
127
+ * - Configure your app's associated domains (iOS) and asset links (Android)
128
+ * - Passkeys require HTTPS domain verification
129
+ *
130
+ * ```ts
131
+ * import { ReactNativePasskeyAuth } from "jazz-tools/react-native-core";
132
+ *
133
+ * const auth = new ReactNativePasskeyAuth(
134
+ * crypto,
135
+ * authenticate,
136
+ * authSecretStorage,
137
+ * "My App",
138
+ * "myapp.com"
139
+ * );
140
+ * ```
141
+ *
142
+ * @category Auth Providers
143
+ */
144
+ export class ReactNativePasskeyAuth {
145
+ constructor(
146
+ protected crypto: CryptoProvider,
147
+ protected authenticate: AuthenticateAccountFunction,
148
+ protected authSecretStorage: AuthSecretStorage,
149
+ public appName: string,
150
+ public rpId: string,
151
+ ) {}
152
+
153
+ static readonly id = "passkey";
154
+
155
+ /**
156
+ * Log in using an existing passkey.
157
+ * This will prompt the user to authenticate with their device biometrics.
158
+ */
159
+ logIn = async () => {
160
+ const { crypto, authenticate } = this;
161
+
162
+ const webAuthNCredential = await this.getPasskeyCredentials();
163
+
164
+ if (!webAuthNCredential) {
165
+ return;
166
+ }
167
+
168
+ if (!webAuthNCredential.response.userHandle) {
169
+ throw new Error("Passkey credential is missing userHandle");
170
+ }
171
+
172
+ const webAuthNCredentialPayload = base64UrlToUint8Array(
173
+ webAuthNCredential.response.userHandle,
174
+ );
175
+
176
+ const accountSecretSeed = webAuthNCredentialPayload.slice(
177
+ 0,
178
+ cojsonInternals.secretSeedLength,
179
+ );
180
+
181
+ const secret = crypto.agentSecretFromSecretSeed(accountSecretSeed);
182
+
183
+ const accountID = cojsonInternals.rawCoIDfromBytes(
184
+ webAuthNCredentialPayload.slice(
185
+ cojsonInternals.secretSeedLength,
186
+ cojsonInternals.secretSeedLength + cojsonInternals.shortHashLength,
187
+ ),
188
+ ) as ID<Account>;
189
+
190
+ await authenticate({
191
+ accountID,
192
+ accountSecret: secret,
193
+ });
194
+
195
+ await this.authSecretStorage.set({
196
+ accountID,
197
+ secretSeed: accountSecretSeed,
198
+ accountSecret: secret,
199
+ provider: "passkey",
200
+ });
201
+ };
202
+
203
+ /**
204
+ * Register a new passkey for the current account.
205
+ * This will create a passkey that stores the account credentials securely on the device.
206
+ *
207
+ * @param username - The display name for the passkey
208
+ */
209
+ signUp = async (username: string) => {
210
+ const credentials = await this.authSecretStorage.get();
211
+
212
+ if (!credentials?.secretSeed) {
213
+ throw new Error(
214
+ "Not enough credentials to register the account with passkey",
215
+ );
216
+ }
217
+
218
+ await this.createPasskeyCredentials({
219
+ accountID: credentials.accountID,
220
+ secretSeed: credentials.secretSeed,
221
+ username,
222
+ });
223
+
224
+ const currentAccount = await Account.getMe().$jazz.ensureLoaded({
225
+ resolve: {
226
+ profile: true,
227
+ },
228
+ });
229
+
230
+ if (username.trim().length !== 0) {
231
+ currentAccount.profile.$jazz.set("name", username);
232
+ }
233
+
234
+ await this.authSecretStorage.set({
235
+ accountID: credentials.accountID,
236
+ secretSeed: credentials.secretSeed,
237
+ accountSecret: credentials.accountSecret,
238
+ provider: "passkey",
239
+ });
240
+ };
241
+
242
+ private async createPasskeyCredentials({
243
+ accountID,
244
+ secretSeed,
245
+ username,
246
+ }: {
247
+ accountID: ID<Account>;
248
+ secretSeed: Uint8Array;
249
+ username: string;
250
+ }) {
251
+ const webAuthNCredentialPayload = new Uint8Array(
252
+ cojsonInternals.secretSeedLength + cojsonInternals.shortHashLength,
253
+ );
254
+
255
+ webAuthNCredentialPayload.set(secretSeed);
256
+ webAuthNCredentialPayload.set(
257
+ cojsonInternals.rawCoIDtoBytes(accountID as unknown as RawAccountID),
258
+ cojsonInternals.secretSeedLength,
259
+ );
260
+
261
+ const challenge = uint8ArrayToBase64Url(
262
+ new Uint8Array(this.crypto.randomBytes(32)),
263
+ );
264
+ const userId = uint8ArrayToBase64Url(webAuthNCredentialPayload);
265
+
266
+ const passkey = getPasskeyModule();
267
+
268
+ try {
269
+ await passkey.create({
270
+ challenge,
271
+ rp: {
272
+ id: this.rpId,
273
+ name: this.appName,
274
+ },
275
+ user: {
276
+ id: userId,
277
+ name: `${username} (${new Date().toLocaleString()})`,
278
+ displayName: username,
279
+ },
280
+ pubKeyCredParams: [
281
+ { alg: -7, type: "public-key" }, // ES256
282
+ { alg: -257, type: "public-key" }, // RS256
283
+ ],
284
+ authenticatorSelection: {
285
+ residentKey: "required",
286
+ userVerification: "preferred",
287
+ },
288
+ timeout: 60000,
289
+ attestation: "none",
290
+ });
291
+ } catch (error) {
292
+ throw new Error("Passkey creation aborted", { cause: error });
293
+ }
294
+ }
295
+
296
+ private async getPasskeyCredentials(): Promise<PasskeyGetResult | null> {
297
+ const challenge = uint8ArrayToBase64Url(
298
+ new Uint8Array(this.crypto.randomBytes(32)),
299
+ );
300
+
301
+ const passkey = getPasskeyModule();
302
+
303
+ try {
304
+ const result = await passkey.get({
305
+ challenge,
306
+ rpId: this.rpId,
307
+ timeout: 60000,
308
+ userVerification: "preferred",
309
+ });
310
+
311
+ return result;
312
+ } catch (error) {
313
+ throw new Error("Passkey authentication aborted", { cause: error });
314
+ }
315
+ }
316
+ }