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,67 @@
|
|
|
1
|
+
import { JazzClerkAuth, type MinimalClerkClient } from "jazz-tools";
|
|
2
|
+
import { getAuthSecretStorage, getJazzContext } from "../jazz.svelte.js";
|
|
3
|
+
import { useIsAuthenticated } from "./useIsAuthenticated.svelte.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Authentication state returned by {@link useClerkAuth}.
|
|
7
|
+
*
|
|
8
|
+
* - `"anonymous"`: User is not authenticated with Jazz (may or may not be signed into Clerk)
|
|
9
|
+
* - `"signedIn"`: User is authenticated with both Clerk and Jazz
|
|
10
|
+
*/
|
|
11
|
+
export type ClerkAuth = {
|
|
12
|
+
state: "anonymous" | "signedIn";
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Registers a Clerk authentication listener and provides reactive auth state.
|
|
17
|
+
*
|
|
18
|
+
* Must be used within a component that is a child of `JazzSvelteProvider`.
|
|
19
|
+
* Automatically syncs Clerk authentication state with Jazz.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```svelte
|
|
23
|
+
* <script>
|
|
24
|
+
* import { useClerkAuth } from "jazz-tools/svelte";
|
|
25
|
+
*
|
|
26
|
+
* const auth = useClerkAuth(clerk);
|
|
27
|
+
* </script>
|
|
28
|
+
*
|
|
29
|
+
* {#if auth.state === "signedIn"}
|
|
30
|
+
* <p>Welcome back!</p>
|
|
31
|
+
* {:else}
|
|
32
|
+
* <p>Please sign in</p>
|
|
33
|
+
* {/if}
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @param clerk - The Clerk client instance
|
|
37
|
+
* @returns An object with a reactive `state` property
|
|
38
|
+
* @throws Error if used in guest mode
|
|
39
|
+
* @category Auth Providers
|
|
40
|
+
*/
|
|
41
|
+
export function useClerkAuth(clerk: MinimalClerkClient): ClerkAuth {
|
|
42
|
+
const context = getJazzContext();
|
|
43
|
+
const authSecretStorage = getAuthSecretStorage();
|
|
44
|
+
|
|
45
|
+
if ("guest" in context.current) {
|
|
46
|
+
throw new Error("Clerk auth is not supported in guest mode");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const authMethod = new JazzClerkAuth(
|
|
50
|
+
context.current.authenticate,
|
|
51
|
+
context.current.logOut,
|
|
52
|
+
authSecretStorage,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
$effect(() => {
|
|
56
|
+
return authMethod.registerListener(clerk);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const isAuthenticated = useIsAuthenticated();
|
|
60
|
+
const state = $derived(isAuthenticated.current ? "signedIn" : "anonymous");
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
get state() {
|
|
64
|
+
return state;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
A pre-configured Jazz provider that integrates with Clerk authentication.
|
|
4
|
+
|
|
5
|
+
Use this component instead of `JazzSvelteProvider` when using Clerk for authentication.
|
|
6
|
+
It handles:
|
|
7
|
+
- Pre-loading Jazz credentials from Clerk before rendering children
|
|
8
|
+
- Registering Clerk auth state listeners
|
|
9
|
+
- Wiring up logout functionality to Clerk's signOut
|
|
10
|
+
|
|
11
|
+
@example
|
|
12
|
+
```svelte
|
|
13
|
+
<JazzSvelteProviderWithClerk
|
|
14
|
+
{clerk}
|
|
15
|
+
sync={{ peer: "wss://cloud.jazz.tools/?key=your-key" }}
|
|
16
|
+
AccountSchema={MyAccountSchema}
|
|
17
|
+
>
|
|
18
|
+
<App />
|
|
19
|
+
</JazzSvelteProviderWithClerk>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
@category Auth Providers
|
|
23
|
+
-->
|
|
24
|
+
<script
|
|
25
|
+
lang="ts"
|
|
26
|
+
generics="S extends (AccountClass<Account> & CoValueFromRaw<Account>) | AnyAccountSchema"
|
|
27
|
+
>
|
|
28
|
+
import {
|
|
29
|
+
Account,
|
|
30
|
+
type AccountClass,
|
|
31
|
+
type AnyAccountSchema,
|
|
32
|
+
type CoValueFromRaw,
|
|
33
|
+
InMemoryKVStore,
|
|
34
|
+
type InstanceOfSchema,
|
|
35
|
+
JazzClerkAuth,
|
|
36
|
+
KvStoreContext,
|
|
37
|
+
type MinimalClerkClient,
|
|
38
|
+
type SyncConfig,
|
|
39
|
+
} from "jazz-tools";
|
|
40
|
+
import {
|
|
41
|
+
type BaseBrowserContextOptions,
|
|
42
|
+
LocalStorageKVStore,
|
|
43
|
+
} from "jazz-tools/browser";
|
|
44
|
+
import type { Snippet } from "svelte";
|
|
45
|
+
import { JazzSvelteProvider } from "../jazz.svelte.js";
|
|
46
|
+
import RegisterClerkAuth from "./RegisterClerkAuth.svelte";
|
|
47
|
+
|
|
48
|
+
type Props = {
|
|
49
|
+
/** The Clerk client instance for authentication (can be null while Clerk is initializing) */
|
|
50
|
+
clerk: MinimalClerkClient | null;
|
|
51
|
+
/** Content to render when provider is initialized */
|
|
52
|
+
children?: Snippet;
|
|
53
|
+
/** Content to render while Clerk auth is loading */
|
|
54
|
+
fallback?: Snippet;
|
|
55
|
+
/** Content to render when authentication initialization fails */
|
|
56
|
+
errorFallback?: Snippet<[Error]>;
|
|
57
|
+
/** Callback when authentication initialization fails */
|
|
58
|
+
onAuthError?: (error: Error) => void;
|
|
59
|
+
/** Enable server-side rendering support with anonymous fallback */
|
|
60
|
+
enableSSR?: boolean;
|
|
61
|
+
/** Custom key for storing auth secrets in KvStore */
|
|
62
|
+
authSecretStorageKey?: string;
|
|
63
|
+
/** Jazz sync configuration (peer URL and key) */
|
|
64
|
+
sync: SyncConfig;
|
|
65
|
+
/** Custom storage implementation for auth secrets */
|
|
66
|
+
storage?: BaseBrowserContextOptions["storage"];
|
|
67
|
+
/** Account schema class for typed account access */
|
|
68
|
+
AccountSchema?: S;
|
|
69
|
+
/** Default profile name for new accounts */
|
|
70
|
+
defaultProfileName?: string;
|
|
71
|
+
/** Callback when an anonymous account is discarded during sign-in */
|
|
72
|
+
onAnonymousAccountDiscarded?: (
|
|
73
|
+
anonymousAccount: InstanceOfSchema<S>,
|
|
74
|
+
) => Promise<void>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
let {
|
|
78
|
+
clerk,
|
|
79
|
+
children,
|
|
80
|
+
fallback,
|
|
81
|
+
errorFallback,
|
|
82
|
+
onAuthError,
|
|
83
|
+
...providerProps
|
|
84
|
+
}: Props = $props();
|
|
85
|
+
|
|
86
|
+
let isLoaded = $state(false);
|
|
87
|
+
let initError = $state<Error | null>(null);
|
|
88
|
+
|
|
89
|
+
function setupKvStore() {
|
|
90
|
+
const isSSR = typeof window === "undefined";
|
|
91
|
+
if (isSSR) {
|
|
92
|
+
console.debug("[Jazz] Using InMemoryKVStore for SSR context");
|
|
93
|
+
}
|
|
94
|
+
KvStoreContext.getInstance().initialize(
|
|
95
|
+
isSSR ? new InMemoryKVStore() : new LocalStorageKVStore(),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Pre-loads Jazz authentication data from Clerk before mounting JazzSvelteProvider.
|
|
101
|
+
*
|
|
102
|
+
* For authenticated Clerk users with existing Jazz credentials, this populates the auth
|
|
103
|
+
* secret storage before rendering children, avoiding a flash of unauthenticated state.
|
|
104
|
+
* For unauthenticated users, the initialization completes immediately with no effect.
|
|
105
|
+
*/
|
|
106
|
+
$effect(() => {
|
|
107
|
+
setupKvStore();
|
|
108
|
+
|
|
109
|
+
if (!clerk) return;
|
|
110
|
+
|
|
111
|
+
let cancelled = false;
|
|
112
|
+
|
|
113
|
+
JazzClerkAuth.initializeAuth(clerk)
|
|
114
|
+
.then(() => {
|
|
115
|
+
if (!cancelled) {
|
|
116
|
+
isLoaded = true;
|
|
117
|
+
initError = null;
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
.catch((error) => {
|
|
121
|
+
console.error(
|
|
122
|
+
"[Jazz] Clerk authentication initialization failed:",
|
|
123
|
+
error,
|
|
124
|
+
);
|
|
125
|
+
if (!cancelled) {
|
|
126
|
+
const errorObj =
|
|
127
|
+
error instanceof Error ? error : new Error(String(error));
|
|
128
|
+
initError = errorObj;
|
|
129
|
+
onAuthError?.(errorObj);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return () => {
|
|
134
|
+
cancelled = true;
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
</script>
|
|
138
|
+
|
|
139
|
+
{#if initError}
|
|
140
|
+
{#if errorFallback}
|
|
141
|
+
{@render errorFallback(initError)}
|
|
142
|
+
{:else}
|
|
143
|
+
<div data-testid="jazz-clerk-auth-error">
|
|
144
|
+
Authentication initialization failed. Please refresh the page or try again
|
|
145
|
+
later.
|
|
146
|
+
</div>
|
|
147
|
+
{/if}
|
|
148
|
+
{:else if isLoaded && clerk}
|
|
149
|
+
<JazzSvelteProvider {...providerProps} onLogOut={clerk.signOut}>
|
|
150
|
+
<RegisterClerkAuth {clerk}>
|
|
151
|
+
{@render children?.()}
|
|
152
|
+
</RegisterClerkAuth>
|
|
153
|
+
</JazzSvelteProvider>
|
|
154
|
+
{:else if fallback}
|
|
155
|
+
{@render fallback?.()}
|
|
156
|
+
{/if}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Internal component that registers the Clerk auth listener.
|
|
4
|
+
|
|
5
|
+
This component exists because `useClerkAuth` requires access to the Jazz context,
|
|
6
|
+
which is only available inside `JazzSvelteProvider`'s children. By placing the
|
|
7
|
+
hook call in this child component, we ensure the context is properly initialized.
|
|
8
|
+
-->
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import type { MinimalClerkClient } from "jazz-tools";
|
|
11
|
+
import type { Snippet } from "svelte";
|
|
12
|
+
import { useClerkAuth } from "./ClerkAuth.svelte.js";
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
clerk: MinimalClerkClient;
|
|
16
|
+
children?: Snippet;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let { clerk, children }: Props = $props();
|
|
20
|
+
|
|
21
|
+
// Register the Clerk auth listener after JazzSvelteProvider context is available.
|
|
22
|
+
// The return value (auth state) is intentionally unused here - this component's
|
|
23
|
+
// sole purpose is to register the listener that syncs Clerk and Jazz auth state.
|
|
24
|
+
useClerkAuth(clerk);
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
{@render children?.()}
|
package/src/svelte/auth/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export * from "./PasskeyAuth.svelte.js";
|
|
2
2
|
export * from "./PassphraseAuth.svelte.js";
|
|
3
|
+
export * from "./ClerkAuth.svelte.js";
|
|
3
4
|
export { default as PasskeyAuthBasicUI } from "./PasskeyAuthBasicUI.svelte";
|
|
5
|
+
export { default as JazzSvelteProviderWithClerk } from "./JazzSvelteProviderWithClerk.svelte";
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Account,
|
|
5
|
+
InMemoryKVStore,
|
|
6
|
+
JazzClerkAuth,
|
|
7
|
+
KvStoreContext,
|
|
8
|
+
} from "jazz-tools";
|
|
9
|
+
import type { MinimalClerkClient } from "jazz-tools";
|
|
10
|
+
import { render as renderSvelte, waitFor } from "@testing-library/svelte";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
createJazzTestAccount,
|
|
14
|
+
createJazzTestGuest,
|
|
15
|
+
setupJazzTestSync,
|
|
16
|
+
} from "../testing";
|
|
17
|
+
import { render, screen } from "./testUtils";
|
|
18
|
+
import TestClerkAuthWrapper from "./TestClerkAuthWrapper.svelte";
|
|
19
|
+
import JazzSvelteProviderWithClerk from "../auth/JazzSvelteProviderWithClerk.svelte";
|
|
20
|
+
|
|
21
|
+
KvStoreContext.getInstance().initialize(new InMemoryKVStore());
|
|
22
|
+
|
|
23
|
+
function createMockClerkClient(
|
|
24
|
+
user: MinimalClerkClient["user"] = null,
|
|
25
|
+
): MinimalClerkClient {
|
|
26
|
+
return {
|
|
27
|
+
user,
|
|
28
|
+
signOut: vi.fn(),
|
|
29
|
+
addListener: vi.fn(() => () => {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("useClerkAuth", () => {
|
|
34
|
+
let account: Account;
|
|
35
|
+
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
await setupJazzTestSync();
|
|
38
|
+
account = await createJazzTestAccount({
|
|
39
|
+
isCurrentActiveAccount: true,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should return anonymous state when not authenticated", async () => {
|
|
44
|
+
const mockClerk = createMockClerkClient();
|
|
45
|
+
|
|
46
|
+
render(
|
|
47
|
+
TestClerkAuthWrapper,
|
|
48
|
+
{ clerk: mockClerk },
|
|
49
|
+
{ account, isAuthenticated: false },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(screen.getByTestId("auth-state").textContent).toBe("anonymous");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should return signedIn state when authenticated", async () => {
|
|
56
|
+
const mockClerk = createMockClerkClient({
|
|
57
|
+
id: "user_123",
|
|
58
|
+
fullName: "Test User",
|
|
59
|
+
username: "testuser",
|
|
60
|
+
firstName: "Test",
|
|
61
|
+
lastName: "User",
|
|
62
|
+
primaryEmailAddress: { emailAddress: "test@example.com" },
|
|
63
|
+
unsafeMetadata: {
|
|
64
|
+
jazzAccountID: "test123",
|
|
65
|
+
jazzAccountSecret: "secret123",
|
|
66
|
+
},
|
|
67
|
+
update: vi.fn(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
render(
|
|
71
|
+
TestClerkAuthWrapper,
|
|
72
|
+
{ clerk: mockClerk },
|
|
73
|
+
{ account, isAuthenticated: true },
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(screen.getByTestId("auth-state").textContent).toBe("signedIn");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should register the clerk listener", async () => {
|
|
80
|
+
const mockClerk = createMockClerkClient();
|
|
81
|
+
|
|
82
|
+
render(
|
|
83
|
+
TestClerkAuthWrapper,
|
|
84
|
+
{ clerk: mockClerk },
|
|
85
|
+
{ account, isAuthenticated: false },
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(mockClerk.addListener).toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should cleanup listener on unmount", async () => {
|
|
92
|
+
const mockUnsubscribe = vi.fn();
|
|
93
|
+
const mockClerk = createMockClerkClient();
|
|
94
|
+
mockClerk.addListener = vi.fn(() => mockUnsubscribe);
|
|
95
|
+
|
|
96
|
+
const { unmount } = render(
|
|
97
|
+
TestClerkAuthWrapper,
|
|
98
|
+
{ clerk: mockClerk },
|
|
99
|
+
{ account, isAuthenticated: false },
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(mockClerk.addListener).toHaveBeenCalled();
|
|
103
|
+
unmount();
|
|
104
|
+
expect(mockUnsubscribe).toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should throw error in guest mode", async () => {
|
|
108
|
+
const guest = await createJazzTestGuest();
|
|
109
|
+
const mockClerk = createMockClerkClient();
|
|
110
|
+
|
|
111
|
+
expect(() => {
|
|
112
|
+
render(TestClerkAuthWrapper, { clerk: mockClerk }, { account: guest });
|
|
113
|
+
}).toThrow("Clerk auth is not supported in guest mode");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should call listener with clerk client events", async () => {
|
|
117
|
+
const mockClerk = createMockClerkClient();
|
|
118
|
+
let listenerCallback: ((data: unknown) => void) | undefined;
|
|
119
|
+
mockClerk.addListener = vi.fn((callback) => {
|
|
120
|
+
listenerCallback = callback;
|
|
121
|
+
return () => {};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
render(
|
|
125
|
+
TestClerkAuthWrapper,
|
|
126
|
+
{ clerk: mockClerk },
|
|
127
|
+
{ account, isAuthenticated: false },
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect(mockClerk.addListener).toHaveBeenCalled();
|
|
131
|
+
expect(listenerCallback).toBeDefined();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("JazzClerkAuth.initializeAuth", () => {
|
|
136
|
+
afterEach(() => {
|
|
137
|
+
vi.restoreAllMocks();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should handle initialization errors gracefully", async () => {
|
|
141
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
142
|
+
const initializeAuthSpy = vi
|
|
143
|
+
.spyOn(JazzClerkAuth, "initializeAuth")
|
|
144
|
+
.mockRejectedValue(new Error("Test error"));
|
|
145
|
+
|
|
146
|
+
const mockClerk = createMockClerkClient();
|
|
147
|
+
|
|
148
|
+
// The error should be thrown by the mock
|
|
149
|
+
await expect(JazzClerkAuth.initializeAuth(mockClerk)).rejects.toThrow(
|
|
150
|
+
"Test error",
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Restore using vitest's cleanup
|
|
154
|
+
initializeAuthSpy.mockRestore();
|
|
155
|
+
consoleSpy.mockRestore();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("JazzSvelteProviderWithClerk", () => {
|
|
160
|
+
afterEach(() => {
|
|
161
|
+
vi.restoreAllMocks();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should render children after successful initialization", async () => {
|
|
165
|
+
const mockClerk = createMockClerkClient();
|
|
166
|
+
vi.spyOn(JazzClerkAuth, "initializeAuth").mockResolvedValue(undefined);
|
|
167
|
+
|
|
168
|
+
const { container } = renderSvelte(JazzSvelteProviderWithClerk, {
|
|
169
|
+
props: {
|
|
170
|
+
clerk: mockClerk,
|
|
171
|
+
sync: { peer: "wss://test.example.com" },
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
await waitFor(() => {
|
|
176
|
+
// After initialization, the provider should render (even without children)
|
|
177
|
+
// The error div should not be present
|
|
178
|
+
expect(
|
|
179
|
+
container.querySelector('[data-testid="jazz-clerk-auth-error"]'),
|
|
180
|
+
).toBeNull();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should show default error message when initialization fails", async () => {
|
|
185
|
+
const mockClerk = createMockClerkClient();
|
|
186
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
187
|
+
vi.spyOn(JazzClerkAuth, "initializeAuth").mockRejectedValue(
|
|
188
|
+
new Error("Init failed"),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const { container } = renderSvelte(JazzSvelteProviderWithClerk, {
|
|
192
|
+
props: {
|
|
193
|
+
clerk: mockClerk,
|
|
194
|
+
sync: { peer: "wss://test.example.com" },
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await waitFor(() => {
|
|
199
|
+
const errorDiv = container.querySelector(
|
|
200
|
+
'[data-testid="jazz-clerk-auth-error"]',
|
|
201
|
+
);
|
|
202
|
+
expect(errorDiv).not.toBeNull();
|
|
203
|
+
expect(errorDiv?.textContent).toContain(
|
|
204
|
+
"Authentication initialization failed",
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
consoleSpy.mockRestore();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should call onAuthError callback when initialization fails", async () => {
|
|
212
|
+
const mockClerk = createMockClerkClient();
|
|
213
|
+
const onAuthError = vi.fn();
|
|
214
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
215
|
+
vi.spyOn(JazzClerkAuth, "initializeAuth").mockRejectedValue(
|
|
216
|
+
new Error("Init failed"),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
renderSvelte(JazzSvelteProviderWithClerk, {
|
|
220
|
+
props: {
|
|
221
|
+
clerk: mockClerk,
|
|
222
|
+
sync: { peer: "wss://test.example.com" },
|
|
223
|
+
onAuthError,
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await waitFor(() => {
|
|
228
|
+
expect(onAuthError).toHaveBeenCalledWith(expect.any(Error));
|
|
229
|
+
expect(onAuthError).toHaveBeenCalledWith(
|
|
230
|
+
expect.objectContaining({ message: "Init failed" }),
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
consoleSpy.mockRestore();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("should not update state after unmount (cancellation)", async () => {
|
|
238
|
+
const mockClerk = createMockClerkClient();
|
|
239
|
+
let resolveInit: () => void;
|
|
240
|
+
const initPromise = new Promise<void>((resolve) => {
|
|
241
|
+
resolveInit = resolve;
|
|
242
|
+
});
|
|
243
|
+
vi.spyOn(JazzClerkAuth, "initializeAuth").mockReturnValue(initPromise);
|
|
244
|
+
|
|
245
|
+
const { unmount } = renderSvelte(JazzSvelteProviderWithClerk, {
|
|
246
|
+
props: {
|
|
247
|
+
clerk: mockClerk,
|
|
248
|
+
sync: { peer: "wss://test.example.com" },
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Unmount before initialization completes
|
|
253
|
+
unmount();
|
|
254
|
+
|
|
255
|
+
// Resolve the promise after unmount - should not cause errors
|
|
256
|
+
resolveInit!();
|
|
257
|
+
|
|
258
|
+
// Give time for any potential state updates
|
|
259
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
260
|
+
|
|
261
|
+
// If we get here without errors, the cancellation worked
|
|
262
|
+
expect(true).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe("useClerkAuth reactive state transitions", () => {
|
|
267
|
+
let account: Account;
|
|
268
|
+
|
|
269
|
+
beforeEach(async () => {
|
|
270
|
+
await setupJazzTestSync();
|
|
271
|
+
account = await createJazzTestAccount({
|
|
272
|
+
isCurrentActiveAccount: true,
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
afterEach(() => {
|
|
277
|
+
vi.restoreAllMocks();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("should update state reactively when listener callback fires", async () => {
|
|
281
|
+
const mockClerk = createMockClerkClient();
|
|
282
|
+
let listenerCallback: ((data: unknown) => void) | undefined;
|
|
283
|
+
mockClerk.addListener = vi.fn((callback) => {
|
|
284
|
+
listenerCallback = callback;
|
|
285
|
+
return () => {};
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const { container } = render(
|
|
289
|
+
TestClerkAuthWrapper,
|
|
290
|
+
{ clerk: mockClerk },
|
|
291
|
+
{ account, isAuthenticated: false },
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// Initial state should be anonymous
|
|
295
|
+
expect(screen.getByTestId("auth-state").textContent).toBe("anonymous");
|
|
296
|
+
|
|
297
|
+
// Verify listener was registered
|
|
298
|
+
expect(listenerCallback).toBeDefined();
|
|
299
|
+
|
|
300
|
+
// Note: Full state transition testing would require more complex setup
|
|
301
|
+
// involving the actual JazzClerkAuth.onClerkUserChange flow.
|
|
302
|
+
// This test verifies the listener registration which is the foundation
|
|
303
|
+
// for reactive updates.
|
|
304
|
+
});
|
|
305
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { MinimalClerkClient } from "jazz-tools";
|
|
3
|
+
import { useClerkAuth } from "../auth/ClerkAuth.svelte.js";
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
clerk: MinimalClerkClient;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
let { clerk }: Props = $props();
|
|
10
|
+
|
|
11
|
+
const auth = useClerkAuth(clerk);
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<div data-testid="clerk-auth-wrapper">
|
|
15
|
+
<div data-testid="auth-state">{auth.state}</div>
|
|
16
|
+
</div>
|
|
@@ -6,6 +6,7 @@ import { JAZZ_AUTH_CTX, JAZZ_CTX } from "../jazz.svelte";
|
|
|
6
6
|
|
|
7
7
|
type JazzExtendedOptions = {
|
|
8
8
|
account: Account | { guest: AnonymousJazzAgent };
|
|
9
|
+
isAuthenticated?: boolean;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
const render = <T extends Component>(
|
|
@@ -13,7 +14,9 @@ const render = <T extends Component>(
|
|
|
13
14
|
props: ComponentProps<T>,
|
|
14
15
|
jazzOptions: JazzExtendedOptions,
|
|
15
16
|
) => {
|
|
16
|
-
const ctx = TestJazzContextManager.fromAccountOrGuest(jazzOptions.account
|
|
17
|
+
const ctx = TestJazzContextManager.fromAccountOrGuest(jazzOptions.account, {
|
|
18
|
+
isAuthenticated: jazzOptions.isAuthenticated,
|
|
19
|
+
});
|
|
17
20
|
|
|
18
21
|
return renderSvelte(
|
|
19
22
|
// @ts-expect-error Svelte new Component type is not compatible with @testing-library/svelte
|
|
@@ -47,7 +47,7 @@ type PermissiveClerkUser = Omit<ClerkUser, "unsafeMetadata" | "update"> & {
|
|
|
47
47
|
export type MinimalClerkClient = {
|
|
48
48
|
user: PermissiveClerkUser | null | undefined;
|
|
49
49
|
signOut: () => Promise<void>;
|
|
50
|
-
addListener: (listener: (data: unknown) => void) => void;
|
|
50
|
+
addListener: (listener: (data: unknown) => void) => (() => void) | void;
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
export type ClerkCredentials = {
|
|
@@ -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?: (
|
|
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)
|
|
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
|
-
|
|
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: {
|