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.
- package/.svelte-kit/__package__/client.d.ts.map +1 -1
- package/.svelte-kit/__package__/client.js +3 -1
- package/.svelte-kit/__package__/server.d.ts.map +1 -1
- package/.svelte-kit/__package__/server.js +9 -7
- package/.svelte-kit/__package__/tests/client.test.js +48 -0
- package/.turbo/turbo-build.log +70 -66
- package/dist/better-auth/auth/client.d.ts.map +1 -1
- package/dist/better-auth/auth/client.js +1 -1
- package/dist/better-auth/auth/client.js.map +1 -1
- package/dist/better-auth/auth/server.d.ts.map +1 -1
- package/dist/better-auth/auth/server.js +4 -4
- package/dist/better-auth/auth/server.js.map +1 -1
- package/dist/better-auth/database-adapter/index.js.map +1 -1
- package/dist/better-auth/database-adapter/repository/generic.d.ts +3 -3
- package/dist/better-auth/database-adapter/repository/session.d.ts +2 -2
- package/dist/better-auth/database-adapter/schema.d.ts +3 -3
- package/dist/better-auth/database-adapter/schema.d.ts.map +1 -1
- package/dist/{chunk-PEHQ7TN2.js → chunk-QCTQH5RS.js} +31 -4
- package/dist/chunk-QCTQH5RS.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react/hooks.d.ts +1 -2
- package/dist/react/hooks.d.ts.map +1 -1
- package/dist/react/index.js +7 -2
- package/dist/react/index.js.map +1 -1
- package/dist/react-core/hooks.d.ts +94 -3
- package/dist/react-core/hooks.d.ts.map +1 -1
- package/dist/react-core/index.js +130 -135
- package/dist/react-core/index.js.map +1 -1
- package/dist/react-core/tests/useCoStates.test.d.ts +2 -0
- package/dist/react-core/tests/useCoStates.test.d.ts.map +1 -0
- package/dist/react-native/chunk-DGUM43GV.js +11 -0
- package/dist/react-native/chunk-DGUM43GV.js.map +1 -0
- package/dist/react-native/crypto.js +2 -0
- package/dist/react-native/crypto.js.map +1 -1
- package/dist/react-native/index.js +544 -29
- package/dist/react-native/index.js.map +1 -1
- package/dist/react-native-core/auth/PasskeyAuth.d.ts +123 -0
- package/dist/react-native-core/auth/PasskeyAuth.d.ts.map +1 -0
- package/dist/react-native-core/auth/PasskeyAuthBasicUI.d.ts +34 -0
- package/dist/react-native-core/auth/PasskeyAuthBasicUI.d.ts.map +1 -0
- package/dist/react-native-core/auth/auth.d.ts +3 -0
- package/dist/react-native-core/auth/auth.d.ts.map +1 -1
- package/dist/react-native-core/auth/passkey-utils.d.ts +16 -0
- package/dist/react-native-core/auth/passkey-utils.d.ts.map +1 -0
- package/dist/react-native-core/auth/usePasskeyAuth.d.ts +48 -0
- package/dist/react-native-core/auth/usePasskeyAuth.d.ts.map +1 -0
- package/dist/react-native-core/chunk-DGUM43GV.js +11 -0
- package/dist/react-native-core/chunk-DGUM43GV.js.map +1 -0
- package/dist/react-native-core/crypto.js +2 -0
- package/dist/react-native-core/crypto.js.map +1 -1
- package/dist/react-native-core/hooks.d.ts +1 -1
- package/dist/react-native-core/hooks.d.ts.map +1 -1
- package/dist/react-native-core/index.js +539 -24
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/react-native-core/tests/PasskeyAuth.test.d.ts +2 -0
- package/dist/react-native-core/tests/PasskeyAuth.test.d.ts.map +1 -0
- package/dist/react-native-core/tests/passkey-utils.test.d.ts +2 -0
- package/dist/react-native-core/tests/passkey-utils.test.d.ts.map +1 -0
- package/dist/svelte/auth/ClerkAuth.svelte.d.ts +38 -0
- package/dist/svelte/auth/ClerkAuth.svelte.d.ts.map +1 -0
- package/dist/svelte/auth/ClerkAuth.svelte.js +47 -0
- package/dist/svelte/auth/JazzSvelteProviderWithClerk.svelte +156 -0
- package/dist/svelte/auth/JazzSvelteProviderWithClerk.svelte.d.ts +67 -0
- package/dist/svelte/auth/JazzSvelteProviderWithClerk.svelte.d.ts.map +1 -0
- package/dist/svelte/auth/RegisterClerkAuth.svelte +27 -0
- package/dist/svelte/auth/RegisterClerkAuth.svelte.d.ts +17 -0
- package/dist/svelte/auth/RegisterClerkAuth.svelte.d.ts.map +1 -0
- package/dist/svelte/auth/index.d.ts +2 -0
- package/dist/svelte/auth/index.d.ts.map +1 -1
- package/dist/svelte/auth/index.js +2 -0
- package/dist/svelte/tests/ClerkAuth.svelte.test.d.ts +2 -0
- package/dist/svelte/tests/ClerkAuth.svelte.test.d.ts.map +1 -0
- package/dist/svelte/tests/ClerkAuth.svelte.test.js +202 -0
- package/dist/svelte/tests/TestClerkAuthWrapper.svelte +16 -0
- package/dist/svelte/tests/TestClerkAuthWrapper.svelte.d.ts +8 -0
- package/dist/svelte/tests/TestClerkAuthWrapper.svelte.d.ts.map +1 -0
- package/dist/svelte/tests/testUtils.d.ts +1 -0
- package/dist/svelte/tests/testUtils.d.ts.map +1 -1
- package/dist/svelte/tests/testUtils.js +3 -1
- package/dist/testing.js +1 -1
- package/dist/tools/auth/clerk/index.d.ts +1 -1
- package/dist/tools/auth/clerk/types.d.ts +1 -1
- package/dist/tools/auth/clerk/types.d.ts.map +1 -1
- package/dist/tools/coValues/account.d.ts +5 -1
- package/dist/tools/coValues/account.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts +30 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/zodCo.d.ts.map +1 -1
- package/dist/tools/subscribe/types.d.ts +1 -1
- package/dist/tools/subscribe/types.d.ts.map +1 -1
- package/dist/tools/testing.d.ts.map +1 -1
- package/package.json +8 -4
- package/src/better-auth/auth/client.ts +3 -1
- package/src/better-auth/auth/server.ts +9 -7
- package/src/better-auth/auth/tests/client.test.ts +66 -2
- package/src/better-auth/database-adapter/repository/generic.ts +3 -3
- package/src/better-auth/database-adapter/repository/session.ts +2 -2
- package/src/better-auth/database-adapter/schema.ts +5 -5
- package/src/react/hooks.tsx +4 -2
- package/src/react-core/hooks.ts +332 -178
- package/src/react-core/tests/useCoState.selector.test.ts +309 -22
- package/src/react-core/tests/useCoStates.test.tsx +414 -0
- package/src/react-native-core/auth/PasskeyAuth.ts +316 -0
- package/src/react-native-core/auth/PasskeyAuthBasicUI.tsx +284 -0
- package/src/react-native-core/auth/auth.ts +3 -0
- package/src/react-native-core/auth/passkey-utils.ts +47 -0
- package/src/react-native-core/auth/usePasskeyAuth.tsx +85 -0
- package/src/react-native-core/hooks.tsx +2 -0
- package/src/react-native-core/tests/PasskeyAuth.test.ts +463 -0
- package/src/react-native-core/tests/passkey-utils.test.ts +144 -0
- package/src/svelte/auth/ClerkAuth.svelte.ts +67 -0
- package/src/svelte/auth/JazzSvelteProviderWithClerk.svelte +156 -0
- package/src/svelte/auth/RegisterClerkAuth.svelte +27 -0
- package/src/svelte/auth/index.ts +2 -0
- package/src/svelte/tests/ClerkAuth.svelte.test.ts +305 -0
- package/src/svelte/tests/TestClerkAuthWrapper.svelte +16 -0
- package/src/svelte/tests/testUtils.ts +4 -1
- package/src/tools/auth/clerk/types.ts +1 -1
- package/src/tools/coValues/account.ts +11 -3
- package/src/tools/implementation/zodSchema/schemaTypes/AccountSchema.ts +27 -1
- package/src/tools/subscribe/types.ts +1 -1
- package/src/tools/tests/account.test.ts +2 -1
- package/src/tools/tests/inbox.test.ts +7 -7
- package/testSetup.ts +4 -0
- package/vitest.config.ts +1 -0
- 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
|
+
});
|